09.什麼是synchronized的重量級鎖?

王有志發表於2023-01-09

大家好,我是王有志。關注王有志,一起聊技術,聊遊戲,聊在外漂泊的生活。

今天我們繼續學習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

鎖的結構

瞭解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

互斥的實現

到目前為止,無論是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個問題(中)聊到過parkparkEvent,底層是透過pthread_cond_waitpthread_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指令執行前,對所有處理器可見。

Tipsvolatile中詳細解釋記憶體屏障。

喚醒的策略

執行釋放鎖和寫入記憶體後,只需要喚醒下一個執行緒來“交接”鎖的使用權。但是有兩個“等待佇列”:cxqEntryList,該從哪個開始喚醒呢?

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收下尾。


好了,今天就到這裡了,Bye~~

相關文章