本文将从线程的生命周期、java内存模型、关键字Volatile、Synchronized、抽象队列同步器AQS之实现ReentrantLock、countDownLatch、Semaphore,以及sleep、wait、join、yield等重点方法等专题逐一为大家深入浅出探究并发编程原理技术。
首先我们要思考,为什么要用多线程。因为天下武功,唯快不破,主要是快,特别是在多核机器下,就好比8辆汽车一个拥有8个主道的大马路上,各自为道,所向披靡。但是如果任由8辆汽车毫无秩序的奔跑,那么肯定会GG,因此需要对它们进行统一管理,依照交通法进行有序前行。下面就是多线程的“交通法”。
1)线程的生命周期:新建-就绪-运行-阻塞-死亡
阻塞:等待阻塞,调用wait(),线程会释放占用的所有资源,jvm会将线程放到等待池,直到其它线程调用了notify或者notifyAll,才会被唤醒,wait是object的方法。
同步阻塞,线程在获取一个对象锁的时候,如果这个对象锁被其他线程所持有,那么jvm会将线程放入锁池中。
其它阻塞,线程调用了sleep,join,线程不会释放占有的资源,或者发了IO请求之时,jvm会将线程置为阻塞状态,直到sleep超时、join等待线程终止或者超时、IO处理完毕时,线程重新转入就绪状态,sleep是thread的方法。
new Thread新建-start()就绪-运行-阻塞-等待wait()、join()-超时等待wait(ms)、sleep(ms)-终止,执行完毕或者异常终止,释放所有资源。

2)java内存模型
主内存,总线,线程的工作内存,cpu执行引擎。
比如一个成员变量flag = 0,线程A:flag=1
主内存中有一个flag = 0,cpu需要将主内存中的flag读取出来,读取主内存中的数据最小单位是缓存行,他们中间有个总线桥,
①cpu先是read一个flag的副本读取到总线桥上,
②load到线程A中的工作内存之中,
③将工作内存中的flag的副本use到执行引擎之中将flag改为1,
④将flag=1结果赋值到工作内存中,工作内存中的flag改为1,
⑤将工作内存中的flag=1装载出来,
⑥将装载出来的flag=1写入到主内存之中。

4)关键字Volatile
并发的三大特性:原子性、有序性、可见性。
Volatile禁止指令重排,保证有序性、
Volatile底层通过MESI缓存一致性协议保证及时可见。
MESI缓存一致性协议底层原理:
比如一个Volatile修饰的成员变量flag = 0,那么主内存中会有一个flag = 0,这个flag在字节码层面有个lock前缀,线程A读取主内存flag = 0到自己的工作内存中,因为有lock前缀标识,flag的状态置为E(独占)。
线程B读取主内存flag = 0到自己的工作内存中,线程B工作内存中的flag的状态置为S(共享)。
线程A通过总线嗅探机制,嗅探到主内存中的flag被线程B读取了,因此,线程A工作内存中的flag的状态也会置为S(共享)。
如果线程B和线程A都想对flag数据进行修改,他们需要对各自本地缓存行进行加锁,加锁之前他们会向总线桥发送本地缓存行修改信号,总线进行裁决,裁决只有一个线程可以胜出加锁,假设裁决A线程加锁成功,A线程中的flag会被置为M(已修改),那么B线程中的flag所在的缓存行会被置为I(已失效),那么这个失效的缓存行会被丢弃,直到线程A将flag数据写回主内存之后,通知到线程B,线程B才会重新去主内存中取flag。
MESI缓存一致性图例

MESI缓存一致性过程图例

4)Synchronized
应用层面:
加在方法上:整个方法同步,粒度粗
加在代码块:同步代码块,粒度细
Synchronized锁的优化:
①锁的消除
根据逃逸分析进行优化

②锁的粗化

③锁的膨胀升级

原理:Synchronized锁升级机制
Sychronized的偏向锁、轻量级锁、重量级锁,根据锁的竞争程度进行升级,
①只有一个线程,偏向锁
②有多个竞争,但是不激烈(排队时间少,执行快),线程自旋,不让出CPU,不进行上下文切换,轻量级锁
③有多个竞争,竞争激烈(排队时间长,执行慢),重量级锁
重量级锁:高度依赖于底层操作系统的维护,调pthread(线程库),这里面涉及到大量的阻塞、互斥量操作,操作系统分为用户空间和内核空间,而JVM运行于用户态,JVM调用底层操作系统,cpu需要进行一轮状态切换,用户态切换到内核态,效率非常低。
5)抽象队列同步器AQS之ReentrantLock的底层实现详解
ReentrantLock是基于AQS框架的一种实现,它是一种互斥锁


特性:公平与非公平
支持可重入:同一个线程多次调用lock方法进行加锁。
可重入意义:方法中有加锁而又调了加锁的方法。 加了几次锁,就要释放几次锁,直到status为0.
ReentrantLock的Lock锁实现机制
自旋、cas算法、LocksSupport、队列
即假设我们有A、B、C三个线程,会先跑到一个循环体中,一起竞争锁,假设A竞争到了锁,那么AQS中的status(初始值为0)就会加一,并且还有个属性会标记当前线程为A,而其他线程则会被放到队列中,在初次放在队列中时,还会去尝试竞争锁(因为A有可能这时已经释放锁了,尽量避免线程阻塞),没有抢到才会被阻塞,如果A释放锁,就会唤醒阻塞中的线程。
非公平锁:此时如果又来了E、F、G线程,如果E、F、G线程与队列中已经被唤醒的线程一起竞争锁,那么这就是非公平锁。
公平锁:如果是公平锁,则E、F、G线程不能直接去竞争锁,而是得先去队列中排队。


6)ReentrantLock中tryLock()和lock()方法的区别
lock()阻塞加锁
tryLock() 非阻塞尝试加锁,消耗CPU,性能好

ReentrantLock中的公平锁和非公平锁的底层实现
底层使用AQS队列来排队。
7)AQS之并发工具类countDownLatch、Semaphore底层原理
CountDownlatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await()线程都将被唤醒.对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。
8)线程池
说到池子,我们脑海里想到:spring的单例池、数据库连接的连接池,线程的线程池。他们的作用在于可以专业的管理这些对象的生命周期,实现这些对象的重用。节省资源,提高性能。
线程池工作原理:
现在我们主线程当中有很多任务需要执行,它会调用excute进行调度,
①线程池就会创建线程去处理这些任务
②当创建的线程数到达核心线程数量时,线程池就会把任务放到队列当中进行排队,并且对其进行监听
③队列任务如果满了,线程池就会继续创建临时线程进行任务处理
④当创建线程数量到达线程池数据最大值时,就会执行任务拒绝策略,对任务进行相应的拒绝策略。
⑤如果队列中的任务执行完毕,5秒时间确定队列为空,则临时创建的线程就会被销毁。
⑥如果设置了核心线程超时时间,那么到了时间,核心线程就会被销毁。
拒绝策略:
直接抛异常策略
丢弃任务策略
替换旧任务策略
自定义拒绝策略:如果我们不想任务被丢弃,我们可以自己实现一个拒绝策略:即把多出来的任务放到第三方空间站中存储(比如redis),并且对连接池进行监听,如果连接池有线程处于空闲状态了,就让它来处理这里的任务。

9)sleep、wait、join、yield
①锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。
②等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify()或notifyAl()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAI()是将等待池的所有线程放到锁池当中。
s1eep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu的调度,获取到cpu资源后就可以持续运行了。而如果s1eep时该线程有锁,那么s1eep不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程序。如果在睡眠期问其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点和wait是一样的。
1、sleep是Thread类的静态本地方法,wait则是Object类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停报作,wait 则多用于多线程之间的通信。
6、sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会里新竞争到锁继续执行的。
yield() 线程暂时让出cpu使用权,进入就绪状态(轻量级)。
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join()那线程B会进入到阻塞队列,直到线程A结束或中断线程。

948

被折叠的 条评论
为什么被折叠?



