09.什么是synchronized的重量级锁?
大家好,我是王有志。关注王有志,一起聊技术,聊游戏,从北漂生活谈到国际风云。
鸽了这么久是给自己找到了冠冕堂皇的理由--羊了。说实话发烧那几天真的很难受,根本不想下床,完成日常工作都已经用尽了全部的力气,根本没经历写文章。
言归正传,今天我们继续学习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~~