小陈:呼叫老王......
老王:来了来了,小陈你准备好了吗?今天我们来讲synchronized的锁重入、锁优化、和锁升级的原理
小陈:早就准备好了,我现在都等不及了
老王:那就好,那我们废话不多说,直接开始第一个话题:synchronized是怎么实现锁重入的?
synchronized的锁重入
小陈:老王,其实这个问题,之前我看了前几篇讲的,就知道了。
老王:哦,看来你是有准备的,那你来说说......
小陈:所谓锁重入,就是支持正在持有锁的线程支持再次获取锁,不会出现自己锁死自己的问题。
老王:嗯嗯,没错的
小陈:我打个比方,比如以下的代码:
synchronized(this) {
synchronized(this){
synchronized(this){
synchronized(this){
synchronized(this){
........
}
}
}
}
}
可能对应下面的指令:
monitorenter
monitorenter
monitorenter
monitorenter
monitorenter
......
monitorexit
monitorexit
monitorexit
monitorexit
monitorexit
回顾之前讲的加锁就是将_count 由 0 设置为1,将_owner指向自己,这里的 _owner就是指向加锁的线程。
(1)所以再次重入加锁的时候,发现有人加锁了,同时检查 _owner是不是自己加锁的,如果是自己加锁的,只需要将_count 次数加1即可。
(2)同样,在释放锁的时候执行monitorexit指令,首先将 _count进行减1,当 _count 减少到0的时候表示自己释放了锁,然后将_owner 指向null。
小陈:所以,根据上诉锁重入的方式,代码进入了5次synchronized 相当于执行了5次monitorenter加锁,最后_count = 5。 当5次monitorexit执行完了之后,_count = 0即释放了锁。
老王:很好,说得很详细,鼓掌鼓掌.......
锁消除
老王:小陈啊,锁重入这个你理解得很不错,锁消除这个你再来说说..
小陈:锁消除啊,这个也很简单,就是在不存在锁竞争的地方使用了synchronized,jvm会自动帮你优化掉,比如说下面的这段代码......
public void business() {
// lock对象方法内部创建,线程私有的,根本不会引起竞争
Object lock = new Object();
synchronized(lock) {
i++;
j++;
// 其它业务操作
}
}
上面的这段代码,由于lock对象是线程私有的,多个线程不会共享;像这种情况多线程之间没有竞争,就没必要使用锁了,就有可能被JVM优化成以下的代码:
public void business() {
i++;
j++;
// 其它业务操作
}
小陈:这就是我理解的锁消除,只有一个线程会用到,不会引起多个线程竞争的;相当于就自己用,没必要加锁了。
老王:嗯嗯,这个锁消除也理解的不错......
synchronized锁升级
老王:那synchronized的锁升级原理呢? 你来说说
小陈:额,这个锁升级其实我了解得不深,还比较模糊,还是老王您来讲吧......
老王:哈哈,好。讲解锁升级之前,我先问个问题,synchronized为什么要设计成可升级的锁呢?
小陈:这个,我理解的就是希望能尽量花费最小的代价能达到目的。
老王:嗯嗯,是这个理由没错;但是你知道synchronized在什么锁的情况下花费什么代价吗?以及每次升级之后花费了什么代价吗?
小陈:额,这个......,不清楚
老王:在说这个之前,我先给你看一下前两章都讲解过Mark Word的图,我们再来回顾一下:
之前我们说过,Mark Word是一个32bit位的数据结构,最后两位表示的是锁标志位,当Mark Word的锁标志位不同的时候,代表Mark Word 中记录的数据不一样。
(1)比如锁模式标志位是,也就是最后两位是01的时候,表示处于无锁模式或者偏向锁模式。
无锁:如果此时偏向锁标志,倒数第3位,是0,即最后3位是001,表示当前处于无锁模式,此时Mark Word就常规记录对象hashcode、GC年龄信息。
偏向锁:倒数第3位是1,即Mark word最后3位是101,则表示当前处于偏向锁模式,那么Mark Word就记录获取了偏向锁的线程ID、对象的GC年龄。
(2)轻量级锁:当锁模式标志位是00的时候,表示当前处于轻量级锁模式,此时会生成一个轻量级的锁记录,存放在获取锁的线程栈空间中,Mark Word此时就存储这个锁记录的地址。
Mark Word存储的地址在哪个线程的栈空间中,就表示哪个线程获取到了轻量级锁。
(3)重量级锁:当锁模式标志位是10的时候,表示当前处于重量级锁模式,此时加锁就不是Mark Word的责任了,需要找monitor锁监视器,这个上一章我们已经讲解monitor加锁的原理了。
此时Mark Word就记录了一下monitor的地址,然后有线程找Mark Word的时候,Mark Word就把monitor地址给它,告诉线程自个根据这个地址找monitor进行加锁。
老王:小陈啊,这个是我们前两章讲解过的内容,这些都还记得不?
小陈:嗯嗯,这些我都知道,老王你前面两章已经分析得非常细致了。
老王:那好,我就不继续啰嗦了,首先马上进入synchronized锁升级过程中,偏向锁的讲解。
偏向锁
如果上表格所示,当有线程第一次进入synchronized的同步代码块之内,发现:
Mark Word的最后三位是001,表示当前无锁状态,说明锁的这时候竞争不激烈啊。
于是选择代价最小的方式,加了个偏向锁,只在第一次获取偏向锁的时候执行CAS操作(将自己的线程Id通过CAS操作设置到Mark Word中),同时将偏向锁标志位改为1。
后面如果自己再获取锁的时候,每次检查一下发现自己之前加了偏向锁,就直接执行代码,就不需要再次加锁了......
老王:说到这里,你知道偏向锁的原理了没?
小陈:明白了,感情线程A这个家伙加锁的时候发现之前没人加过锁,所以这家伙很自私,加了个偏向锁指向了自己,后面自己再进入synchronized的时候就不需要加锁了,嘿嘿,原来是这样啊
老王:没错,就是这样......
老王:加了偏向锁的人确实是个自私的人,这家伙用完了锁之后,自己加锁时候修改过的Mark Word信息都不会再改回来了,也就是它不会主动释放锁。
小陈:啊这...,这个哥们不释放锁,如果它用完了,别人这个时候需要进入synchronized代码块怎么办?
老王:你说的这个问题啊,其实JVM的设计者也考虑到了,这就涉及到一个重偏向的问题。
偏向锁之重偏向
老王:我给你举个例子说明一下重偏向咋回事:
线程B去申请加锁,发现是线程A加了偏向锁;这时候回去判断一下线程A是否存活,如果线程A挂了,就可以重新偏向了,重偏向也就是将自己的线程ID设置到Mark Word中。
如果线程A没挂,但是synchronized代码块执行完了,这个时候也可以重新偏向了,将偏向标识指向自己,轮到我了,哈哈。
老王:小陈啊,这就回答了你的问题了,线程A用完了这家伙不把Mark Word标识改回来;没关系啊,线程B判断线程A没在synchronized同步代码块了,就执行重新偏向了。
小陈:嗯嗯,老王你这么将我就明白了。
小陈:老王啊,我还有个问题,就是如果线程B在申请获取锁的时候,线程A这哥们还没执行完synchronized同步代码块怎么办?
老王:这个时候就有锁的竞争了,这就需要将锁升级一下了,线程B就会把锁升级为轻量级锁?
偏向锁为什么要升级为轻量级锁?
小陈:为啥啊,都使用偏向锁不行吗?不升级有什么坏处?
老王:下面给你讲原因,先给你看下如下代码块:
// 代码块1
synchronized(this){
// 业务代码1
}
// 代码块2
synchronized(this){
// 业务代码2
}
// 代码块3
synchronized(this){
// 业务代码3
}
// 代码块4
synchronized(this){
// 业务代码4
}
假如这个时候有线程A、B、C、D四个线程,线程A先加了偏向锁。之前讲过偏向锁只是在第一次获取锁的时候加锁,后面都是直接操作的不需要加锁。
这个时候其它几个线程B、C、D想要加锁,如果线程A连续执行上面4个代码块,那么其他线程看到线程A都在执行synchronized同步代码块,没完没了了,想重偏向都不行!! ,这个时候就需要等线程A执行完4个synchronized代码块之后才能获取锁啊,哈哈,别的线程都只能看线程A一个人自己在那表演了,这样代码就变成串行执行了。
小陈:原来是在这样啊...,也就是说如果不进行升级,就会存在这种问题,明白了。
老王:这下子多个线程竞争锁的时候为什么要升级明白了吧?
小陈:懂了懂......
老王:下面我们进入锁升级的第一个级别,轻量级锁,讲之前,先回顾之前将的一个知识点:
轻量级锁
轻量级锁模式下,加锁之前会创建一个锁记录,然后将Mark Word中的数据备份到锁记录中(Mark Word存储hashcode、GC年龄等很重要数据,不能丢失了),以便后续恢复Mark Word使用。
这个锁记录放在加锁线程的虚拟机栈中,加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中,就说明哪个线程获取了轻量级锁。
小陈:嗯嗯,这个我了解,之前我们在前面的文章里面讨论过。
老王:好的,既然你了解我就放心了,记得如果不了解的话需要看一下之前讲过的文章哦......
老王:就好比下面的图,线程A获取了轻量级锁,锁记录存在线程A的虚拟机栈中,然后Mark Word的前面30位存储锁记录的地址。
老王:了解了轻量级加锁的原理之后,我们继续,来讲讲偏向锁升级为轻量级锁的过程:
(1)首先线程A持有偏向锁,然后正在执行synchronized块中的代码
(2)这个时候线程B来竞争锁,发现有人加了偏向锁并且正在执行synchronized块中的代码,为了避免上述说的线程A一直持有锁不释放的情况,需要对锁进行升级,升级为轻量级锁
(3)先将线程A暂停,为线程A创建一个锁记录Lock Record,将Mark Word的数据复制到锁记录中;然后将锁记录放入线程A的虚拟机栈中
(4)然后将Mark Word中的前30位指向线程A中锁记录的地址,将线程A唤醒,线程A就知道自己持有了轻量级锁
老王:上面就是偏向锁升级为轻量级锁的过程,小陈你看明白了吗?
小陈:等等,我再消化一下,10分钟过后.....(再重新看一遍)
小陈:老王,你说的这个偏向锁升级轻量级锁的过程我看懂了...
小陈:老王啊,上面偏向锁升级为轻量级锁的过程和原理我了解了,那在轻量级锁模式下,多线程是怎么竞争锁和释放锁的?
老王:我再慢慢给你讲解下:
(1)线程A和线程B同时竞争锁,在轻量级锁模式下,都会创建Lock Record锁记录放入自己的栈帧中
(2)同时执行CAS操作,将Mark Word前30位设置为自己锁记录的地址,谁设置成功了,锁就获取到锁
老王:上面讲了加锁的过程,轻量级锁的释放很简单,就将自己的Lock Record中的Mark Word备份的数据恢复回去即可,恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。
老王:这个轻量级锁的加锁和释放锁原理懂了没?
小陈:嗯嗯,清晰明了了,老王真棒......
老王:好了看看时间,差不多九点半了,讲完最后一个轻量级锁升级为重量级锁就差不多下班了
重量级锁的自旋
老王:小陈啊,你想想,在轻量级锁模式下获取锁失败的线程应该会怎么样?
小陈:获取锁失败的线程应该会再去尝试吧?或者直接沉睡等待别人释放锁的时候将它唤醒?
老王:你说的两种其实都有可能,但是你觉得哪种花销会更小一点?
小陈:我觉得线程沉睡花费代价更大吧,这涉及到上下文切换,操作系统层次涉及到用户态转内核态,是一个非常重的操作。
老王:你说的没错,既然线程沉睡和唤醒代价这么大,所以肯定是不会让线程轻易就沉睡的;
比如说线程沉睡再唤醒最少需要3000ms的时间,如果某个线程只使用锁150ms的时间就释放了,如果直接采用沉睡方式的话,这个时候synchronized的性能就太差了。
所以啊JVM的设计者,设计了一种方案,获取锁失败之后的线程自己先原地等一段时间,然后再去重试获取锁,这种方式就叫做自旋。
小陈:但是JVM怎么知道要等多久呢,加入持有锁的那个人一直不释放锁,其他人要一直自旋等待,然后不断重复尝试吗?这样不是非常消耗CPU的资源的吗?
老王:这里自旋多少次是有一个限制的,之前我们讲解monitor的底层原理的时候就讲解过了,如果忘记的话可以回去重新看一下。
monitor有一个 _spinFreq参数表示最大自旋的次数, _spinClock参数表示自旋的间隔时间。所以自旋最多会重试_spinFreq次,每次失败之后等 _spinClock的时间过后再去重试,如果尝试_spinFreq次之后都没有成功,那没辙了,只能沉睡了。
老王:自旋其实是非常消耗CPU资源的,自旋期间相当于CPU啥也不干,就在那等着的。为了避免自旋时间太长,所以JVM就规定了默认最多自旋10次,10次还获取不到锁,那就直接将线程挂起了,线程就会直接阻塞等待了,这个时候性能就差了。
老王:小陈啊,关于这个重量级锁下的自旋过程,你清楚了没?
小陈:嗯嗯,非常了解了,老王牛逼......
总结
总的来说啊,JVM设计的这套synchronized锁升级的原则,主要是为了花费最小的代价能达到加锁的目的;
比如在没有竞争的情况下,进入synchronized的使用使用偏向锁就够了,这样只需要第一次执行CAS操作获取锁,获取了偏向锁之后,后面每次进入synchronized同步代码块就不需要再次加锁了。
然后在存在多个线程竞争锁的时候就不能使用偏向锁了,不能只偏心一个人,它优先获取锁,别人都看它表演,这样是不行的。
于是就升级为轻量级锁,在轻量级锁模式在每次加锁和释放是都需要执行CAS操作,对比偏向锁来说性能低一点的,但是总体还是比较轻量级的。
为了尽量提升线程获取锁的机会,避免线程陷入获取锁失败就立即沉睡的局面(线程沉睡再唤醒涉及上下文切换,用户态内核态切换,是一个非常重的操作,很费时间),所以设计自旋等待;线程每次自旋一段时间之后再去重试获取锁。
当竞争非常激烈,并发很高,或者是synchronized代码块执行耗时比较长,就会积压大量的线程都在自旋,由于自旋是空耗费CPU资源的,也就是CPU在那等着,做不了其他事情,所以在尝试了最大的自旋次数之后;及时释放CPU资源,将线程挂起了。
老王:总的来说synchronized升级的原理就是这样了?
小陈:嗯嗯,讲解的非常详细了,真棒......
老王:好了,本章的讲解就到这里了,synchronized也讲解的差不多了,下一章最后讲解一下synchronized保证并发安全的可见性、有序性、原子性是怎么做到的?
那么我们这个《练气篇》就差不多了,学完这一篇之后,你会发现对volatile、synchronized的了解更加深入了,在并发底层保证了什么,怎么做到了?相信你学完这一篇之后,功力就会更加不少咯。
小陈:好的,老王,我们下一章见。
目录
JAVA并发专题 《筑基篇》
1.什么是CPU多级缓存模型?
2.什么是JAVA内存模型?
3.线程安全之可见性、有序性、原子性是什么?
4.什么是MESI缓存一致性协议?怎么解决并发的可见性问题?
JAVA并发专题《练气篇》
5.volatile怎么保证可见性?
6.什么是内存屏障?具有什么作用?
7.volatile怎么通过内存屏障保证可见性和有序性?
8.volatile为啥不能保证原子性?
9.synchronized是个啥东西?应该怎么使用?
10.synchronized底层之monitor、对象头、Mark Word?
11.synchronized底层是怎么通过monitor进行加锁的?
12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁
13.synchronized怎么保证可见性、有序性、原子性?
JAVA并发专题《结丹篇》
15.unsafe类的CAS是怎么保证原子性的?
16.Atomic原子类体系讲解
17.AtomicInteger、AtomicBoolean的底层原理
18.AtomicReference、AtomicStampReference底层原理
19.Atomic中的LongAdder底层原理之分段锁机制
20.Atmoic系列Strimped64分段锁底层实现源码剖析
JAVA并发专题《金丹篇》
21.AQS是个啥?为啥说它是JAVA并发工具基础框架?
22.基于AQS的互斥锁底层源码深度剖析
23.基于AQS的共享锁底层源码深度剖析
24.ReentrantLock是怎么基于AQS实现独占锁的?
25.ReentrantLock的Condition机制底层源码剖析
26.CountDownLatch 门栓底层源码和实现机制深度剖析
27.CyclicBarrier 栅栏底层源码和实现机制深度剖析
28.Semaphore 信号量底层源码和实现机深度剖析
29.ReentrantReadWriteLock 读写锁怎么表示?
JAVA并发专题《元神篇》并发数据结构篇
31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能?
32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能?
33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能?
34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现?
35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样?
36.DelayQueue 底层源码剖析,延时队列怎么实现?
37.SynchronousQueue底层原理解析
JAVA并发专题《飞升篇》线程池底层深度剖析
39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思?
40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的?
ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?
ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?
ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?
ThreadPoolExecutor shutdown、shutdownNow内部核心流程
再回头看看为啥不推荐Executors提供几种线程池?
ThreadPoolExecutor线程池篇总结