09.什么是synchronized的重量级锁? - 王有志
source link: https://www.cnblogs.com/wyz1994/p/17035081.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
大家好,我是王有志。关注王有志,一起聊技术,聊游戏,聊在外漂泊的生活。
今天我们继续学习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~~
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK