您現在的位置是:首頁 > 人文

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

由 程式那點事 發表于 人文2022-09-30
簡介輕量級鎖解鎖流程如下(基於使用lock record重入計數的情況):遍歷棧的Lock Record,如果_displaced_header 為 NULL,表明鎖是可重入的,跳過不作處理如果_displaced_header 不為 NULL

資料庫被鎖了怎麼解鎖

前言

上篇主要對 Synchronized 的鎖實現原理 Monitor 機制進行了介紹,由於 Monitor 基於作業系統呼叫,上下文切換導致開銷大,在競爭不激烈時效能不算很好, 在 jdk6 之後進了系列最佳化。前文對最佳化措施進行了簡單介紹,下面將一一介紹這些最佳化的細節,行文思路大致如下:

從重量級鎖的最佳化開始講,一是自旋鎖,二是儘量避免進入 Monitor ,即使用輕量級鎖

講解輕量級鎖及加鎖解鎖流程

輕量級鎖在沒有競爭時,每次重入仍然需要執行cas操作,為解決這個問題,因而產生了偏向鎖

詳細介紹偏向鎖

Synchronized 鎖的細節

一、自旋鎖

自旋鎖比較簡單,邏輯在上篇也已經進行過闡述,這一篇章我們著重看下它的效能如何?

在競爭度較小的時候,重量級鎖的上下文切換導致的開銷相對於 CPU 處理任務的時間佔比較重,此種情況下,自旋鎖的效能有優勢,因自旋而導致的 CPU 浪費在可接受範圍內;當競爭激烈的時候,繼續使用自旋鎖則得不償失,效能上比直接使用重量級鎖要差,大量的等待鎖的時間被浪費。

根據任務處理時間不同,自旋鎖表現也不一,在任務持續時間長的情況下,自旋太久顯然是對 CPU 時間片的浪費,且因任務持續時間長,在 10 此預設自旋次數的情況下,易出現自旋結束也無法獲取到鎖,那麼此次空轉就是毫無收益的效能浪費。在任務處理時間較短的情況下,顯然自旋獲得鎖的機率要大,因此如果對要執行的任務有很明確的處理時長認知,可以根據情況適當的調整初始自旋次數,JVM 引數為:-XX:PreBlockSpin。

二、輕量級鎖

根據觀察,多執行緒中並不總是存在著競爭,使用輕量級鎖避免了鎖 Monitor 這繁重的資料結構,輕量級鎖通常只鎖一個欄位(鎖記錄),在 HotSpot 中的實現是在當前執行緒的棧幀中建立鎖記錄結構(Lock Record)。

1。輕量級鎖加鎖流程

在當前執行緒的棧幀中建立 Lock Record

構建一個無鎖狀態的 Displaced Mark Word

將 Displaced Mark Word 儲存到 Lock Record 中的 _displaced_header 屬性

CAS 更新 Displaced Mark Word 指標,注意【3】是將 Lock record 的 header 的值設定成一個 displaced mark word,【4】這一步是將當前物件頭的 Mark Word 中的高30 位(全文都是隻針對 32 位虛擬機器來談)指向 Lock Record 中的 header。

4。1 CAS 成功,執行同步程式碼塊

4。2 CAS 失敗,存在兩種情況

3。2。1 判斷是否為鎖重入(關於輕量級鎖的可重入有疑問,見下文)

3。2。2 鎖被其他執行緒佔有,需要競爭鎖,進入鎖膨脹過程

加鎖成功的話,當前物件的 Mark Word 後兩位鎖標誌位置為 00,餘下高位作為指標儲存 Lock Record 的地址

輕量級鎖加鎖原始碼如下:

// traditional lightweight locking if (!success) { // markOop就是物件頭結構, 生成物件頭,這個物件頭的狀態設定為無鎖,生成的這個物件頭就是displaced Mark word markOop displaced = lockee->mark()->set_unlocked(); // 將 displaced Mark word 設定到 lock record 的 _displaced_header 欄位 entry->lock()->set_displaced_header(displaced); // 判斷JVM引數-XX:+UseHeavyMonitors 是否設定了只有重量級鎖 bool call_vm = UseHeavyMonitors; // cmpxchg_ptr即 cas 交換指令,將當前物件頭的 Mark Word 中的高30 位指向 Lock Record 中的 header 使用重量級鎖或者CAS 失敗進入這個if塊 if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) { // Is it simple recursive case? 是否為鎖重入 if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { entry->lock()->set_displaced_header(NULL); } else { CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } } }

2。輕量級鎖重入的疑問

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

關於輕量級鎖的重入,實現方式主要有兩種,一是如 Monitor 一樣透過一個變數來計數,二是每重入一次都生成一個 Lock Record,對Lock Record 的個數計數來隱士計數。在查詢資料的過程中發現,大部分的說法是 HotSpot 選擇第二種實現方式。

第一個疑問是為何會選擇第二種實現方式,是否對空間造成了一定的浪費,生成 Lock Record 相比整型加一操作效能消耗應該也更大,不知道 HotSpot 作何考量選擇此種方式。下圖為此種實現方式下的輕量級鎖結構。

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

輕量級鎖結構

2。 第一點提出的是多次生成鎖記錄,但是在程式碼中沒有看到如何重新生成鎖記錄(如有大佬解惑,不勝感激),是否 lock() 函式建立了一個新的 Lock Record?

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

3。輕量級鎖解鎖流程如下(基於使用lock record重入計數的情況):

遍歷棧的Lock Record,如果_displaced_header 為 NULL,表明鎖是可重入的,跳過不作處理

如果_displaced_header 不為 NULL,即最後一個鎖記錄,呼叫 CAS 將 _displaced_header 恢復到當前物件頭,解鎖成功

偏向鎖

It also follows the principle of optimizing com- mon cases。 The observation exploited is the biased distri- bution of lockers called thread locality。 That is, for a given object, the lock tends to be dominantly acquired and re- leased by a specific thread, which is obviously the case in single-threaded applications [2]

根據觀察結果來看,多執行緒下很多時候會出現以下情況:一個執行緒在頻繁的釋放和加鎖;即多執行緒實際上已經退化成了單執行緒線性執行,在這種情況下,減少 CAS 這種原子操作,也能提高效能。偏向鎖的原理是為執行緒保留鎖,Mark Word 中儲存 ThreadId,只有第一次需要進行 CAS 操作將這個欄位設定為當前執行緒的執行緒ID,後續加鎖的時候只需要檢視 ThreadId 是否指向自己,而輕量級鎖每次鎖欄位都需要進行 CAS 操作。

1。偏向鎖加鎖流程如下:

檢查鎖是否可偏向,物件頭低位倒數第三位為1(即後三位的值為 0x5)表明可偏向

如果可偏向,首先判斷 Mark Word 的內容是否是當前執行緒ID,

2。1 是,執行同步程式碼

2。2 不是,執行 CAS 將 Mark Word 的高位設定為當前執行緒ID, CAS 執行分以下情況:

2。2。1 執行成功,則加鎖成功

2。2。2 執行失敗,說明此鎖已經偏向了其他執行緒,因為產生了競爭所以撤銷偏向鎖,進入輕量級鎖加鎖流程

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

偏向鎖加鎖流程

2。偏向鎖與 HashCode 的關係

由於Hash碼必須是唯一的,即 hashcode() 方法只能被呼叫一次,因此產生了以下規則來保證hash code 的唯一性:

HashCode 是懶載入,當呼叫 hashcode() 方法的時候,生成的 hash code 才會儲存到物件頭指定位置

當一個物件已經呼叫過 hashcode() 方法,那麼偏向鎖狀態會置為0,無法進入偏向鎖狀態,直接進入輕量級鎖

如果一個物件現在已經處於偏向鎖狀態,在同步程式碼塊中需要執行 hashcode() 方法,則偏向鎖會撤銷,進入重量級鎖

3。偏向鎖狀態下 Mark World 的情況

在無鎖狀態下,如果沒有呼叫 hashcode() ,高 25 位未使用,如果呼叫過 hashcode(),則儲存的是 hash code,只有當高 25 位未使用時,才能進入偏向鎖,mark word 儲存獲取到鎖的執行緒ID,當鎖撤銷的時候,恢復為無鎖狀態,即高 25 位為 NULL。

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

偏向鎖

4。偏向鎖的可重入

在一些部落格中發現以下說法:使用偏向鎖時,每重入一次建立一個Lock Record。這個說法毫無疑問是錯誤的,偏向鎖在重入的時候只檢查 ThreadId,是自己的執行緒 Id 就可以執行同步程式碼塊;解鎖則只需要看是否是偏向模式,因此完全沒有必要進行重入計數,生成 Lock Record 來計數就更沒有這個必要了。

5。什麼時候偏向鎖不可用

升級為輕量級鎖之後,當一個執行緒持有鎖,另一個執行緒來競爭鎖的時候 CAS 失敗,就會將低三位 101 設定為 001,即不可偏向,這也是鎖可升級不可降級的原因。

發生了批次撤銷後,就不會再進入偏向鎖了

6。JDK 15 偏向鎖已經被禁用

JDK 15 開始預設不使用偏向鎖,且相關的命令列指令被標記為過時

多執行緒與高併發(二)——Synchronized 加鎖解鎖流程

Biased locking introduced a lot of complex code into the synchronization subsystem and is invasive to other HotSpot components as well。 This complexity is a barrier to understanding various parts of the code and an impediment to making significant design changes within the synchronization subsystem。 To that end we would like to disable, deprecate, and eventually remove support for biased locking。[3]

偏向鎖在同步子系統中引入了許多複雜的程式碼,並且還侵入了其他 HotSpot 元件。這種複雜性造成了對程式碼各個部分的理解障礙,也阻礙了同步子系統進行重大設計更改。為此,我們希望禁用、棄用並最終移除對偏向鎖的支援。

偏向鎖的撤銷

1。何為撤銷?

撤銷是指當物件處於偏向鎖模式的時候,不再使用偏向鎖,且標記不可偏向(低位 001);注意撤銷不是常規意義上的解鎖,偏向鎖的解鎖是當鎖處於偏向鎖狀態時,同步塊執行完畢,需要對鎖進行釋放,只需要檢查是否處於已偏向(此處檢查兩個引數,一是偏向位即低位倒數第三位為1,為 1 即表示可偏向也可表示已偏向,故還需要檢查 ThreadID 不為空),如果處於偏向鎖模式,則直接 return 釋放鎖成功。撤銷與解鎖的區別是撤銷需要將偏向鎖標識位置為0,標記該物件不可偏向。

撤銷操作不是必須在安全點操作,首先會嘗試在不安全點使用 CAS 操作修改 Mark Word 為無鎖狀態,如果嘗試失敗會等待在安全點(JVM 概念)撤銷,等待安全點的操作開銷很大,即需要STW。

2。觸發撤銷的條件

執行緒 A 首先獲取了偏向鎖,此時來了執行緒 B 嘗試對鎖偏向,發現鎖已經被偏向 A 執行緒,B 執行緒會觸發鎖的偏向撤銷並進一步膨脹成輕量級鎖。

觸發了批次撤銷

呼叫 wait()/notify() 觸發重量級鎖

3。 什麼是批次重偏向

當一個類產生了大量物件,線上程 A 訪問這些物件時,所有物件偏向A,執行緒 A 釋放鎖後,執行緒 B 訪問這些所有物件,每個物件都會觸發鎖的撤銷升級成輕量級鎖,這個撤銷的次數達到一定閾值(預設20次),JVM 就會把該類產生的所有物件的偏向狀態偏向到 B,這就是批次重偏向。批次重偏向的重點即避免進入輕量級鎖,由於 A B的競爭導致多個物件都進入了輕量級鎖,而透過撤銷的閾值判斷髮現大多數執行緒都偏向了 B,那麼只需要將此類的所有物件都修改成偏向 B 就可以大機率的避免進入輕量級鎖。

舉個例子:一個類一共生成了30個物件,A 執行緒訪問了 30 個,這 30 個物件都偏向 A,接著 B 只訪問了 25 個物件,前20個物件都由於競爭升級成了輕量級鎖,由於超過閾值 20 觸發了批次重偏向,後續 10 個物件的偏向執行緒 ID 也被修改為執行緒 B,執行緒 B 訪問第 21 個之後的物件都只需要使用偏向鎖,無需使用輕量級鎖。

4。 批次撤銷

與批次重偏向同理,都是某個類的物件頻繁撤銷鎖偏向,撤銷次數達到一定閾值(預設40次),就會觸發以下操作:將次類的物件的偏向鎖標記置為 0,即鎖不再可偏向,新建的物件也是不可偏向的,若再次發生鎖競爭,直接進入輕量級鎖。

可以看到,批次重偏向和批次撤銷的操作是都是對撤銷操作的最佳化,批次重偏向是第一階段的最佳化,批次撤銷是在第一階段最佳化沒有奏效的情況下第二階段的最佳化,所以很明顯,批次撤銷的閾值應該設定的比批次重偏向的大。

三、結語

Synchronized 的原理和最佳化就暫且講到這,兩篇文章主要都是對概念的介紹、各個狀態的鎖的結構介紹和闡述簡化後的流程。還有諸多細節沒有進行敘述,例如重量級鎖就沒有講到重入、鎖膨脹的過程、偏向撤銷的流程、串聯起來整個加鎖的流程,如此種種細節皆略過了,一是精力有限,二是水平有限(個人拙見,要理清這些細節和流程必須自己親自看原始碼,然現階段讀 C ++ 原始碼略為吃力),故而等後續有更多的理解後再補充。文中必然有疏漏或是錯誤,若有發現,還請海涵並指正。

Reference

[1] Evaluating and improving biased locking in the HotSpot virtual machine。

[2] Lock Reservation: Java Locks Can Mostly Do Without Atomic Operations

[3] [JEP 374: Deprecate and Disable Biased Locking]:

https://openjdk。org/jeps/374

推薦文章