缘由:很明显多线程就是为了提高办事的效率,因为单线程的处理效率相对来说越来越达不到要求了,随着硬件这块的提升(CPU多核的出现),这样也提高了CPU的使用率,不至于导致资源的浪费,多个任务可以并行执行,如果一次执行多个任务,如果其中一个出现了阻塞,可能会导致与当前任务无关的的任务也会阻塞,这里就引入了线程(根据不同的任务去创建不同的线程)这样就可以避免不相关的的任务之间相互不受影响。线程可以理解轻量级的进程,一个进程可能有很多个线程组成的
如上图,单核的CPU任何时候都只有一个任务在执行,CPU时间片的不停切换,让你以为是并行执行
线程的应用
方式:1、继承(extend) 2、实现( Runnable)3、实现 Callable 接口通过 k FutureTask 包装器来创建 Thread 线程
场景:大部分还是通过批量操作,或者执行的数据量较大的时候,分线程执行,提升效率
线程相关的基础概念以及状态的转换
6个状态:new(新建), runnable(执行),blocked(阻塞),wating(等待),time_wating(超时等待) ,terminated(终止)
new(新建):创建一个Thread对象,但还未调用start()启动线程,处于初始态。
runnable(执行):运行状态,Java中,运行态包括就绪态和运行态,就绪状态就是获得 了需要执行的资源(即满足执行的条件了,就等CPU的执行命令了,存入在就绪队列中),运行态就是获得了CPU的执行权,正在执行。
blocked(阻塞):即正在执行的线程,在调用某一资源是没获取到,那么就会进入阻塞状态。在Java中,阻塞态(同步阻塞)专指请求锁失败时进入的状态。由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。PS:锁、IO、Socket等都资源。阻塞分为:1、等待阻塞:运行了wait方法,jvm会将其放入等待队列。2、同步阻塞 3、其他阻塞,执行sleep,join,I/O请求,jvm会讲将当前线程设为阻塞,等结束了sleep,join,I/O等再恢复之前状态。
wating(等待):当前线程中调用wait、join、park函数时,当前线程就会进入等待态。也有一个等待队列存放所有等待态的线程。线程处于等待态表示它需要等待其他线程的指示才能继续运行。进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
time_wating(超时等待):超时等待,超时后自动返回
terminated(终止):终止,当前线程执行完毕
引发的问题
很显然如果多线程并行(共享变量)的情况下,是否会有数据的安全性问题!当然这种问题其实大家都早就了解过的,但是为什么会导致这个问题,原因就是:多个线程都几乎同时去请求这个共享变量,并且改变了,但是线程之间的数据时独立的,并不是共享的,所以导致每个线程第一次获取变量时时一样的,但是各自修改之后就导致了变量的不一致了。那么如何处理这个问题呢? 很容易想到的就是加锁(悲观锁,乐观锁)
在java中提供加锁的方式就是Synchronizd关键字的方式 (保证了:可见性,原子性)
这里就先简单的介绍下这个关键字的用法 1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁 2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。不同的修饰类型,代表锁的控制粒度
关于这个锁的粒度这边就在说下:当你传入的是这个对象的所属大小(1、this 2、Object 比较灵活有传入的参数决定 3、class 类层面)等 这边就不再赘述了(注意:synchronized括号后的对象不同,那么锁也就不同了)
既然说到了锁,那么我首先想到的就是锁是如何实现的,应该包含算法吧? 是否需要存储下来,又是怎么存储的?
这个Lock对象有个markword的概念, 记录了对象和锁有关的信息,当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系
如果一个线程加锁了,那么其效率必然会受到影响的,那么不加锁又不行,加锁又会影响效率。该怎么办呢? 那就是该加锁就加锁,不该加锁尽量不加。这就有了后面的锁的升级 (偏向锁 - > 轻量级锁 - > 重量级锁)
1、 偏向锁 (适用于一个线程,没有竞争的情况下)
原理:当一个线程访问同步代码块的时候,对象头会里面当前线程的ID ,这样下次访问的访问的时候,cas比较当前线程ID是否为同一个就是可以了 ,就不需要反复的加锁和释放锁了。如cas失败说明存在所竞争,就需要锁升级了(轻量级锁)
2、轻量级锁(2个线程)
原理:轻量级锁死由于通过偏向锁进行升级得到了,说明存在竞争,就是在cas的时候没有成功,因此得到了轻量级锁。轻量级锁用到了自旋(for循环,执行一段获取锁的逻辑)。当然自旋次数也是一定的次数的,并非无限循环的。JDk1.6之后已有优化(根据前一次的自旋时间,以及拥有者的状态来决定的)
3、重量级锁(多个线程)
原理: 当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了(每个对象会有monitor) ObjectMonitor重量级锁的核心,Monitor锁监视器。只有当monitorenter(获得监视器对象) monitorexit(释放锁),所以只有当释放了,才能获得锁,否则就会阻塞,阻塞之后会将阻塞的线程加入都队列中,,只有当monitorexit,就会去队列中唤醒其他处于阻塞中的线程,去重新竞争锁。
线程之间的通信 (wait/notify 、notifyAll)
线程通过这种通信机制来执行线程的等待/唤醒。例如:如果线程A拥有锁,线程B再去抢占锁时,显然自己抢占不到的,因此线程B需要等待,但是线程B 可以什么获取锁,什么时候可以再去取抢占锁呢?那就需要通过这个机制,线程A以及释放锁,可以去抢占了。