首页IT科技重量级锁轻量级锁(09.什么是synchronized的重量级锁?)

重量级锁轻量级锁(09.什么是synchronized的重量级锁?)

时间2025-05-03 16:20:03分类IT科技浏览7132
导读:大家好,我是王有志。关注王有志,一起聊技术,聊游戏,聊在外漂泊的生活。点击这里查看王有志的资源分享,点击这里参加抽奖活动。...

大家好            ,我是王有志            。关注王有志                    ,一起聊技术      ,聊游戏         ,聊在外漂泊的生活                    。点击这里查看王有志的资源分享                    ,点击这里参加抽奖活动      。

今天我们继续学习synchronized的升级过程         ,目前只剩下最后一步了:轻量级锁->重量级锁         。

通过今天的内容      ,希望能帮助大家解答synchronized都问啥?中除锁粗化                    ,锁消除以及Java 8对synchronized的优化外全部的问题                    。

获取重量级锁

从源码揭秘偏向锁的升级 最后            ,看到synchronizer#slow_enter如果存在竞争   ,会调用ObjectSynchronizer::inflate方法                    ,进行轻量级锁的升级(膨胀)         。

Tips

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { ...... ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD); }

通过ObjectSynchronizer::inflate获取重量级锁ObjectMonitor                ,然后执行ObjectMonitor::enter方法      。

Tips

关于线程你必须知道的8个问题(中)中提到过该方法; 问题是锁升级(膨胀),但重点不在ObjectSynchronizer::inflate                ,因此代码分析放在重量级锁源码分析中                    。

锁的结构

了解ObjectMonitor::enter的逻辑前                    ,先来看ObjectMonitor的结构:

class ObjectMonitor { private: // 保存与ObjectMonitor关联Object的markOop volatile markOop _header; // 与ObjectMonitor关联的Object void* volatile _object; protected: // ObjectMonitor的拥有者 void * volatile _owner; // 递归计数 volatile intptr_t _recursions; // 等待线程队列   ,cxq移入/Object.notify唤醒的线程 ObjectWaiter * volatile _EntryList; private: // 竞争队列 ObjectWaiter * volatile _cxq; // ObjectMonitor的维护线程 Thread * volatile _Responsible; protected: // 线程挂起队列(调用Object.wait) ObjectWaiter * volatile _WaitSet; }

_header字段存储了Object的markOop            ,为什么要这样?因为锁升级后没有空间存储Object的markOop了                    ,存储到_header中是为了在退出时能够恢复到加锁前的状态            。

Tips

实际上basicLock也存储了对象的markOop; EntryList中等待线程来自于cxq移入      ,或Object.notify唤醒但未执行   。

重入的实现

objectMonito#enter方法可以拆成三个部分         ,首先是竞争成功或重入的场景

// 获取当前线程Self Thread * const Self = THREAD; // CAS抢占锁                    ,如果失败则返回_owner void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL); if (cur == NULL) { // CAS抢占锁成功直接返回 return; } // CAS失败场景 // 重量级锁重入 if (cur == Self) { // 递归计数+1 _recursions++; return; } // 当前线程是否曾持有轻量级锁 // 可以看做是特殊的重入 if (Self->is_lock_owned ((address)cur)) { // 递归计数器置为1 _recursions = 1; _owner = Self; return; }

重入和升级的场景中         ,都会操作_recursions                    。_recursions记录了进入ObjectMonitor的次数      ,解锁时要经历相应次数的退出操作才能完成解锁                。

适应性自旋

以上都是成功获取锁的场景                    ,那么产生竞争导致失败的场景是怎样的呢?来看适应性自旋的部分            ,ObjectMonitor倒数第二次对“轻量             ”的追求

// 尝试自旋来竞争锁 Self->_Stalled = intptr_t(this); if (Knob_SpinEarly && TrySpin (Self) > 0) { Self->_Stalled = 0; return; }

objectMonitor#TrySpin方法是对适应性自旋的支持。Java 1.6后加入   ,移除默认次数的自旋                    ,将自旋次数的决定权交给JVM                。

JVM根据锁上一次自旋情况决定                ,如果刚刚自旋成功,并且持有锁的线程正在执行                ,JVM会允许再次尝试自旋                    。如果该锁的自旋经常失败                    ,那么JVM会直接跳过自旋过程   。

Tips

适应性自旋的原码分析放在了重量级锁源码分析中; objectMonitor#TryLock非常简单   ,关键技术依旧是CAS            。

互斥的实现

到目前为止            ,无论是CAS还是自旋                    ,都是偏向锁和轻量级锁中出现过的技术      ,为什么会让ObjectMonitor背上“重量级                  ”的名声呢?

最后是竞争失败的场景:

// 此处省略了修改当前线程状态的代码 for (;;) { EnterI(THREAD); }

实际上         ,进入ObjectMonitor#EnterI后也是先尝试“轻量级       ”的加锁方式:

void ObjectMonitor::EnterI(TRAPS) { if (TryLock (Self) > 0) { return; } if (TrySpin (Self) > 0) { return; } }

接来下是重量级的真正实现:

// 将当前线程(Self)封装为ObjectWaiter的node ObjectWaiter node(Self); Self->_ParkEvent->reset(); node._prev = (ObjectWaiter *) 0xBAD; node.TState = ObjectWaiter::TS_CXQ; // 将node插入到cxq的头部 ObjectWaiter * nxt; for (;;) { node._next = nxt = _cxq; if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt) break; // 为了减少插入到cxq头部的次数                    ,试试能否直接获取到锁 if (TryLock (Self) > 0) { return; } }

逻辑一目了然         ,封装ObjectWaiter对象      ,并加入到cxq队列头部                    。接着往下执行:

// 将当前线程(Self)设置为当前ObjectMonitor的维护线程(_Responsible) // SyncFlags的默认值为0                    ,可以通过-XX:SyncFlags设置 if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) { Atomic::replace_if_null(Self, &_Responsible); } for (;;) { // 尝试设置_Responsible if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::replace_if_null(Self, &_Responsible); } // park当前线程 if (_Responsible == Self || (SyncFlags & 1)) { Self->_ParkEvent->park((jlong) recheckInterval); // 简单的退避算法            ,recheckInterval从1ms开始 recheckInterval *= 8; if (recheckInterval > MAX_RECHECK_INTERVAL) { recheckInterval = MAX_RECHECK_INTERVAL; } } else { Self->_ParkEvent->park(); } // 尝试获取锁 if (TryLock(Self) > 0) break; if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0) break; if (_succ == Self) _succ = NULL; }

逻辑也不复杂   ,不断的park当前线程                    ,被唤醒后尝试获取锁      。需要关注-XX:SyncFlags的设置:

当SyncFlags == 0时                ,synchronized直接挂起线程; 当SyncFlags == 1时,synchronized将线程挂起指定时间         。

前者是永久挂起                ,需要被其它线程唤醒                    ,而后者挂起指定的时间后自动唤醒                    。

Tips:关于线程你必须知道的8个问题(中)聊到过park和parkEvent   ,底层是通过pthread_cond_wait和pthread_cond_timedwait实现的         。

释放重量级锁

释放重量级锁的源码和注释非常长            ,我们省略大部分内容                    ,只看关键部分      。

重入锁退出

我们知道      ,重入是不断增加_recursions的计数         ,那么退出重入的场景就非常简单了:

void ObjectMonitor::exit(bool not_suspended, TRAPS) { Thread * const Self = THREAD; // 第二次持有锁时                    ,_recursions == 1 // 重入场景只需要退出重入即可 if (_recursions != 0) { _recursions--; return; } ..... }

不断的减少_recursions的计数                    。

释放和写入

JVM的实现中         ,当前线程是锁的持有者且没有重入时      ,首先会释放自己持有的锁                    ,接着将改动写入到内存中            ,最后还肩负着唤醒下一个线程的责任            。先来看释放和写入内存的逻辑:

// 置空锁的持有者 OrderAccess::release_store(&_owner, (void*)NULL); // storeload屏障   , OrderAccess::storeload(); // 没有竞争线程则直接退出 if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { TEVENT(Inflated exit - simple egress); return; }

storeload屏障                    ,对于如下语句:

store1; storeLoad; load2

保证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~~

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
seo网站优化上排名推广教程(seo网站优化快速排名) 苹果6蓝牙怎么连接不上汽车(苹果iPhone6s蓝牙连接汽车方法)