大家好,我是王有志。關注王有志,一起聊技術,聊遊戲,聊在外漂泊的生活。
今天我們繼續學習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:
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:
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:
到目前為止,無論是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~~