大家好,重量我是重量王有志,欢迎和我聊技术,重量聊漂泊在外的重量生活。快来加入我们的重量Java提桶跑路群:共同富裕的Java人。 鸽了这么久是重量给自己找到了冠冕堂皇的理由—羊了。说实话发烧那几天真的重量很难受,根本不想下床,重量完成日常工作都已经用尽了全部的重量力气,根本没精力写文章。重量 言归正传,重量今天我们继续学习synchronized的重量升级过程,目前只剩下最后一步了:轻量级锁->重量级锁。重量 通过今天的重量内容,希望能帮助大家解答synchronized都问啥?重量中除锁粗化,锁消除以及Java 8对synchronized的优化外全部的问题。 从源码揭秘偏向锁的升级 最后,看到synchronizer#slow_enter如果存在竞争,会调用ObjectSynchronizer::inflate方法,进行轻量级锁的升级(膨胀)。服务器租用 Tips: 通过ObjectSynchronizer::inflate获取重量级锁ObjectMonitor,然后执行ObjectMonitor::enter方法。 Tips: 关于线程你必须知道的8个问题(中)中提到过该方法; 问题是锁升级(膨胀),但重点不在ObjectSynchronizer::inflate,因此代码分析放在重量级锁源码分析中。 了解ObjectMonitor::enter的逻辑前,先来看ObjectMonitor的结构: _header字段存储了Object的markOop,为什么要这样?因为锁升级后没有空间存储Object的markOop了,存储到_header中是为了在退出时能够恢复到加锁前的状态。 Tips: 实际上basicLock也存储了对象的markOop; EntryList中等待线程来自于cxq移入,或Object.notify唤醒但未执行。 objectMonito#enter方法可以拆成三个部分,首先是竞争成功或重入的场景: 重入和升级的场景中,都会操作_recursions。_recursions记录了进入ObjectMonitor的次数,解锁时要经历相应次数的退出操作才能完成解锁。 以上都是成功获取锁的场景,那么产生竞争导致失败的场景是怎样的呢?来看适应性自旋的部分,ObjectMonitor倒数第二次对“轻量”的追求: objectMonitor#TrySpin方法是对适应性自旋的支持。Java 1.6后加入,移除默认次数的自旋,将自旋次数的决定权交给JVM。 JVM根据锁上一次自旋情况决定,如果刚刚自旋成功,并且持有锁的线程正在执行,JVM会允许再次尝试自旋。如果该锁的自旋经常失败,高防服务器那么JVM会直接跳过自旋过程。 Tips: 适应性自旋的原码分析放在了重量级锁源码分析中; objectMonitor#TryLock非常简单,关键技术依旧是CAS。 到目前为止,无论是CAS还是自旋,都是偏向锁和轻量级锁中出现过的技术,为什么会让ObjectMonitor背上“重量级”的名声呢? 最后是竞争失败的场景: 实际上,进入ObjectMonitor#EnterI后也是先尝试“轻量级”的加锁方式: 接来下是重量级的真正实现: 逻辑一目了然,封装ObjectWaiter对象,并加入到cxq队列头部。接着往下执行: 逻辑也不复杂,不断的park当前线程,被唤醒后尝试获取锁。需要关注-XX:SyncFlags的设置: 当SyncFlags == 0时,synchronized直接挂起线程; 当SyncFlags == 1时,synchronized将线程挂起指定时间。 前者是挂起,需要被其它线程唤醒,而后者挂起指定的时间后自动唤醒。 Tips:关于线程你必须知道的8个问题(中)聊到过park和parkEvent,底层是通过pthread_cond_wait和pthread_cond_timedwait实现的。 释放重量级锁的源码和注释非常长,我们省略大部分内容,只看关键部分。 我们知道,重入是不断增加_recursions的计数,那么退出重入的场景就非常简单了: 不断的减少_recursions的计数。 JVM的实现中,当前线程是锁的持有者且没有重入时,首先会释放自己持有的锁,接着将改动写入到内存中,最后还肩负着唤醒下一个线程的责任。先来看释放和写入内存的逻辑: storeload屏障,对于如下语句: 保证store1指令的写入在load2指令执行前,对所有处理器可见。 Tips:volatile中详细解释内存屏障。 执行释放锁和写入内存后,只需要唤醒下一个线程来“交接”锁的使用权。但是有两个“等待队列”:cxq和EntryList,该从哪个开始唤醒呢? Java 11前,根据QMode来选择不同的策略: QMode == 0,默认策略,将cxq放入EntryList; QMode == 1,翻转cxq,并放入EntryList; QMode == 2,直接从cxq中唤醒; QMode == 3,将cxq移入到EntryList的尾部; QMode == 4,将cxq移入到EntryList的头部。 不同的策略导致了不同的唤醒顺序,现在你知道为什么说synchronized是非公平锁了吧? objectMonitor#ExitEpilog方法就很简单了,调用的是与park对应的unpark方法,这里就不多说了。 Tips:Java 12的objectMonitor移除了QMode,也就是说只有一种唤醒策略了。 我们对重量级锁做个总结。synchronized的重量级锁是ObjectMonitor,它使用到的关键技术有CAS和park。相较于mutex#Monitor来说,它们的本质相同,对park的封装,但ObjectMonitor是做了大量优化的复杂实现。 我们看到了重量级锁是如何实现重入性的,以及唤醒策略导致的“不公平”。那么我们常说的synchronized保证了原子性,有序性和可见性,是如何实现的呢? 大家可以先思考下这个问题,下篇文章会做一个全方位的总结,给synchronized收下尾。 好了,今天就到这里了,Bye~~获取重量级锁
锁的结构
重入的实现
适应性自旋
互斥的实现
释放重量级锁
重入锁退出
释放和写入
唤醒的策略
总结