封面:什么是synchronized的重量级锁?.png

王有志,一个分享硬核Java技术的互金摸鱼侠 加入Java人的提桶跑路群:共同富裕的Java人

今天我们继续学习synchronized的升级过程,目前只剩下最后一步了:轻量级锁->重量级锁。
通过今天的内容,希望能帮助大家解答synchronized都问啥?中除锁粗化,锁消除以及Java 8对synchronized的优化外全部的问题。

获取重量级锁

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

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

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

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

    锁的结构

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

    1. class ObjectMonitor {
    2. private:
    3. // 保存与ObjectMonitor关联Object的markOop
    4. volatile markOop _header;
    5. // 与ObjectMonitor关联的Object
    6. void* volatile _object;
    7. protected:
    8. // ObjectMonitor的拥有者
    9. void * volatile _owner;
    10. // 递归计数
    11. volatile intptr_t _recursions;
    12. // 等待线程队列,cxq移入/Object.notify唤醒的线程
    13. ObjectWaiter * volatile _EntryList;
    14. private:
    15. // 竞争队列
    16. ObjectWaiter * volatile _cxq;
    17. // ObjectMonitor的维护线程
    18. Thread * volatile _Responsible;
    19. protected:
    20. // 线程挂起队列(调用Object.wait)
    21. ObjectWaiter * volatile _WaitSet;
    22. }

    _header字段存储了Object的markOop,为什么要这样?因为锁升级后没有空间存储Object的markOop了,存储到_header中是为了在退出时能够恢复到加锁前的状态
    图1:对象头结构.png
    Tips

  • 实际上basicLock也存储了对象的markOop;

  • EntryList中等待线程来自于cxq移入,或Object.notify唤醒但未执行。

    重入的实现

    objectMonito#enter方法可以拆成三个部分,首先是竞争成功或重入的场景: ```cpp // 获取当前线程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; }

  1. 重入和升级的场景中,都会操作_recursions_recursions记录了进入ObjectMonitor的次数,解锁时要经历相应次数的退出操作才能完成解锁。
  2. <a name="16b2a97f"></a>
  3. ### 适应性自旋
  4. 以上都是成功获取锁的场景,那么产生竞争导致失败的场景是怎样的呢?来看[适应性自旋](https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/hotspot/share/runtime/objectMonitor.cpp#l295)的部分,**ObjectMonitor倒数第二次对“轻量”的追求**:
  5. ```cpp
  6. // 尝试自旋来竞争锁
  7. Self->_Stalled = intptr_t(this);
  8. if (Knob_SpinEarly && TrySpin (Self) > 0) {
  9. Self->_Stalled = 0;
  10. return;
  11. }

objectMonitor#TrySpin方法是对适应性自旋的支持。Java 1.6后加入,移除默认次数的自旋,将自旋次数的决定权交给JVM。
JVM根据锁上一次自旋情况决定,如果刚刚自旋成功,并且持有锁的线程正在执行,JVM会允许再次尝试自旋。如果该锁的自旋经常失败,那么JVM会直接跳过自旋过程
Tips

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

    互斥的实现

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

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

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

    1. void ObjectMonitor::EnterI(TRAPS) {
    2. if (TryLock (Self) > 0) {
    3. return;
    4. }
    5. if (TrySpin (Self) > 0) {
    6. return;
    7. }
    8. }

    接来下是重量级的真正实现: ```cpp // 将当前线程(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;

  1. // 为了减少插入到cxq头部的次数,试试能否直接获取到锁
  2. if (TryLock (Self) > 0) {
  3. return;
  4. }

}

  1. 逻辑一目了然,封装[ObjectWaiter](https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/hotspot/share/runtime/objectMonitor.hpp#l42)对象,并加入到cxq队列头部。接着往下执行:
  2. ```cpp
  3. // 将当前线程(Self)设置为当前ObjectMonitor的维护线程(_Responsible)
  4. // SyncFlags的默认值为0,可以通过-XX:SyncFlags设置
  5. if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
  6. Atomic::replace_if_null(Self, &_Responsible);
  7. }
  8. for (;;) {
  9. // 尝试设置_Responsible
  10. if ((SyncFlags & 2) && _Responsible == NULL) {
  11. Atomic::replace_if_null(Self, &_Responsible);
  12. }
  13. // park当前线程
  14. if (_Responsible == Self || (SyncFlags & 1)) {
  15. Self->_ParkEvent->park((jlong) recheckInterval);
  16. // 简单的退避算法,recheckInterval从1ms开始
  17. recheckInterval *= 8;
  18. if (recheckInterval > MAX_RECHECK_INTERVAL) {
  19. recheckInterval = MAX_RECHECK_INTERVAL;
  20. }
  21. } else {
  22. Self->_ParkEvent->park();
  23. }
  24. // 尝试获取锁
  25. if (TryLock(Self) > 0)
  26. break;
  27. if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0)
  28. break;
  29. if (_succ == Self)
  30. _succ = NULL;
  31. }

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

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

前者是永久挂起,需要被其它线程唤醒,而后者挂起指定的时间后自动唤醒
Tips关于线程你必须知道的8个问题(中)聊到过park和parkEvent,底层是通过pthread_cond_wait和pthread_cond_timedwait实现的。

释放重量级锁

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

重入锁退出

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

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

不断的减少_recursions的计数。

释放和写入

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

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

storeload屏障,对于如下语句:

  1. store1;
  2. storeLoad;
  3. 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方法,这里就不多说了。
TipsJava 12的objectMonitor移除了QMode,也就是说只有一种唤醒策略了。

总结

我们对重量级锁做个总结。synchronized的重量级锁是ObjectMonitor,它使用到的关键技术有CAS和park。相较于mutex#Monitor来说,它们的本质相同,对park的封装,但ObjectMonitor是做了大量优化的复杂实现。
我们看到了重量级锁是如何实现重入性的,以及唤醒策略导致的“不公平”。那么我们常说的synchronized保证了原子性,有序性和可见性,是如何实现的呢?
大家可以先思考下这个问题,下篇文章会做一个全方位的总结,给synchronized收下尾。


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!