12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁 | 您所在的位置:网站首页 › synchronized重量级锁怎么实现的 › 12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁 |
小陈:呼叫老王...... 老王:来了来了,小陈你准备好了吗?今天我们来讲synchronized的锁重入、锁优化、和锁升级的原理 小陈:早就准备好了,我现在都等不及了 老王:那就好,那我们废话不多说,直接开始第一个话题:synchronized是怎么实现锁重入的? synchronized的锁重入小陈:老王,其实这个问题,之前我看了前几篇讲的,就知道了。 老王:哦,看来你是有准备的,那你来说说...... 小陈:所谓锁重入,就是支持正在持有锁的线程支持再次获取锁,不会出现自己锁死自己的问题。 老王:嗯嗯,没错的 小陈:我打个比方,比如以下的代码: 代码语言:javascript复制synchronized(this) { synchronized(this){ synchronized(this){ synchronized(this){ synchronized(this){ ........ } } } } }可能对应下面的指令: 代码语言:javascript复制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会自动帮你优化掉,比如说下面的这段代码...... 代码语言:javascript复制public void business() { // lock对象方法内部创建,线程私有的,根本不会引起竞争 Object lock = new Object(); synchronized(lock) { i++; j++; // 其它业务操作 } }上面的这段代码,由于lock对象是线程私有的,多个线程不会共享;像这种情况多线程之间没有竞争,就没必要使用锁了,就有可能被JVM优化成以下的代码: 代码语言:javascript复制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就会把锁升级为轻量级锁? 偏向锁为什么要升级为轻量级锁?小陈:为啥啊,都使用偏向锁不行吗?不升级有什么坏处? 老王:下面给你讲原因,先给你看下如下代码块: 代码语言:javascript复制// 代码块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并发专题《结丹篇》JDK底层Unsafe类是个啥东西?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 读写锁怎么表示? ReentrantReadWriteLock 读写锁底层源码和机制深度剖析JAVA并发专题《元神篇》并发数据结构篇31.CopyOnAarrayList 底层分析,怎么通过写时复制副本,提升并发性能? 32.ConcurrentLinkedQueue 底层分析,CAS 无锁化操作提升并发性能? 33.ConcurrentHashMap详解,底层怎么通过分段锁提升并发性能? 34.LinkedBlockedQueue 阻塞队列怎么通过ReentrantLock和Condition实现? 35.ArrayBlockedQueued 阻塞队列实现思路竟然和LinkedBlockedQueue一样? 36.DelayQueue 底层源码剖析,延时队列怎么实现? 37.SynchronousQueue底层原理解析 JAVA并发专题《飞升篇》线程池底层深度剖析什么是线程池?看看JDK提供了哪些默认的线程池?底层竟然都是基于ThreadPoolExecutor的?39.ThreadPoolExecutor 构造函数有哪些参数?这些参数分别表示什么意思? 40.内部有哪些变量,怎么表示线程池状态和线程数,看看道格.李大神是怎么设计的? ThreadPoolExecutor execute执行流程?怎么进行任务提交的?addWorker方法干了啥?什么是workder?ThreadPoolExecutor execute执行流程?何时将任务提交到阻塞队列? 阻塞队列满会发生什么?ThreadPoolExecutor 中的Worker是如何执行提交到线程池的任务的?多余Worker怎么在超出空闲时间后被干掉的?ThreadPoolExecutor shutdown、shutdownNow内部核心流程再回头看看为啥不推荐Executors提供几种线程池?ThreadPoolExecutor线程池篇总结 |
CopyRight 2018-2019 实验室设备网 版权所有 |