synchronized四種鎖狀態的升級

goldenJet發表於2019-04-05

一、背景

在 Java 語言中,使用 Synchronized 是能夠實現執行緒同步的,即加鎖。並且實現的是悲觀鎖,在操作同步資源的時候直接先加鎖。

加鎖可以使一段程式碼在同一時間只有一個執行緒可以訪問,在增加安全性的同時,犧牲掉的是程式的執行效能,所以為了在一定程度上減少獲得鎖和釋放鎖帶來的效能消耗,在 jdk6 之後便引入了“偏向鎖”和“輕量級鎖”,所以總共有4種鎖狀態,級別由低到高依次為:無鎖狀態偏向鎖狀態輕量級鎖狀態重量級鎖狀態。這幾個狀態會隨著競爭情況逐漸升級。

注意:鎖可以升級但不能降級。

鎖狀態說明及升級圖示

當然了,在談這四種狀態之前,我們還是有必要再簡單瞭解下 synchronized 的原理。

在使用 synchronized 來同步程式碼塊的時候,經編譯後,會在程式碼塊的起始位置插入 monitorenter指令,在結束或異常處插入 **monitorexit指令。**當執行到 monitorenter 指令時,將會嘗試獲取物件所對應的 **monitor **的所有權,即嘗試獲得物件的鎖。而 synchronized 用的鎖是存放在 Java物件頭 中的。

所以引出了兩個關鍵詞:“Java 物件頭” 和 “Monitor”。

二、Java 物件頭和 Monitor

1、Java 物件頭

我們以 Hotspot 虛擬機器為例,Hotspot 的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Klass Pointer(型別指標)。

Mark Word:預設儲存物件的 HashCode,分代年齡和鎖標誌位資訊。這些資訊都是與物件自身定義無關的資料,所以 Mark Word 被設計成一個非固定的資料結構以便在極小的空間記憶體儲存儘量多的資料。它會根據物件的狀態複用自己的儲存空間,也就是說在執行期間 Mark Word 裡儲存的資料會隨著鎖標誌位的變化而變化。

Klass Point:物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

2、Monitor

Monitor 可以理解為一個同步工具或一種同步機制,通常被描述為一個物件。每一個 Java 物件就有一把看不見的鎖,稱為內部鎖或者 Monitor 鎖。

Monitor 是執行緒私有的資料結構,每一個執行緒都有一個可用 monitor record 列表,同時還有一個全域性的可用列表。每一個被鎖住的物件都會和一個 monitor 關聯,同時 monitor 中有一個 Owner 欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。

三、無鎖

無鎖是指沒有對資源進行鎖定,所有的執行緒都能訪問並修改同一個資源,但同時只有一個執行緒能修改成功。

無鎖的特點是修改操作會在迴圈內進行,執行緒會不斷的嘗試修改共享資源。如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。如果有多個執行緒修改同一個值,必定會有一個執行緒能修改成功,而其他修改失敗的執行緒會不斷重試直到修改成功。

四、偏向鎖

偏向鎖是指當一段同步程式碼一直被同一個執行緒所訪問時,即不存在多個執行緒的競爭時,那麼該執行緒在後續訪問時便會自動獲得鎖,從而降低獲取鎖帶來的消耗,即提高效能。

當一個執行緒訪問同步程式碼塊並獲取鎖時,會在 Mark Word 裡儲存鎖偏向的執行緒 ID。線上程進入和退出同步塊時不再通過 CAS 操作來加鎖和解鎖,而是檢測 Mark Word 裡是否儲存著指向當前執行緒的偏向鎖。輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令即可。

偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒是不會主動釋放偏向鎖的。

關於偏向鎖的撤銷,需要等待全域性安全點,即在某個時間點上沒有位元組碼正在執行時,它會先暫停擁有偏向鎖的執行緒,然後判斷鎖物件是否處於被鎖定狀態。如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態,並撤銷偏向鎖,恢復到無鎖(標誌位為01)或輕量級鎖(標誌位為00)的狀態。

偏向鎖在 JDK 6 及之後版本的 JVM 裡是預設啟用的。可以通過 JVM 引數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程式預設會進入輕量級鎖狀態。

五、輕量級鎖

輕量級鎖是指當鎖是偏向鎖的時候,卻被另外的執行緒所訪問,此時偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋(關於自旋的介紹見文末)的形式嘗試獲取鎖,執行緒不會阻塞,從而提高效能。

輕量級鎖的獲取主要由兩種情況:① 當關閉偏向鎖功能時;② 由於多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖。

在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態,虛擬機器將首先在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的 Mark Word 的拷貝,然後將物件頭中的 Mark Word 複製到鎖記錄中。

拷貝成功後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指標,並將 Lock Record 裡的 owner 指標指向物件的 Mark Word。

如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件 Mark Word 的鎖標誌位設定為“00”,表示此物件處於輕量級鎖定狀態。

如果輕量級鎖的更新操作失敗了,虛擬機器首先會檢查物件的 Mark Word 是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明多個執行緒競爭鎖。

若當前只有一個等待執行緒,則該執行緒將通過自旋進行等待。但是當自旋超過一定的次數時,輕量級鎖便會升級為重量級鎖(鎖膨脹)。

另外,當一個執行緒已持有鎖,另一個執行緒在自旋,而此時又有第三個執行緒來訪時,輕量級鎖也會升級為重量級鎖(鎖膨脹)。

六、重量級鎖

重量級鎖是指當有一個執行緒獲取鎖之後,其餘所有等待獲取該鎖的執行緒都會處於阻塞狀態。

重量級鎖通過物件內部的監視器(monitor)實現,而其中 monitor 的本質是依賴於底層作業系統的 Mutex Lock 實現,作業系統實現執行緒之間的切換需要從使用者態切換到核心態,切換成本非常高。

簡言之,就是所有的控制權都交給了作業系統,由作業系統來負責執行緒間的排程和執行緒的狀態變更。而這樣會出現頻繁地對執行緒執行狀態的切換,執行緒的掛起和喚醒,從而消耗大量的系統資源,導致效能低下。

七、關於自旋

關於自旋,簡言之就是讓執行緒喝杯咖啡小憩一下,用程式碼解釋就是:

do  {
    // do something
}  while  (自旋的規則,或者說自旋的次數)
複製程式碼

引入自旋這一規則的原因其實也很簡單,因為阻塞或喚醒一個 Java 執行緒需要作業系統切換 CPU 狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步程式碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長。並且在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換執行緒,這部分操作的開銷其實是得不償失的。

所以,在物理機器有多個處理器的情況下,當兩個或以上的執行緒同時並行執行時,我們就可以讓後面那個請求鎖的執行緒不放棄 CPU 的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。而為了讓當前執行緒“稍等一下”,我們需讓當前執行緒進行自旋。如果在自旋完成後前面鎖定同步資源的執行緒已經釋放了鎖,那麼當前執行緒就可以不必阻塞而是直接獲取同步資源,從而避免切換執行緒的開銷。

自旋鎖本身是有缺點的,它不能代替阻塞。自旋等待雖然避免了執行緒切換的開銷,但它要佔用處理器時間。如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的執行緒只會白浪費處理器資源。

所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(預設是10次,可以使用 -XX:PreBlockSpin 來更改)沒有成功獲得鎖,就應當掛起執行緒。

自旋鎖在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 來開啟。JDK 6 中變為預設開啟,並且引入了自適應的自旋鎖(適應性自旋鎖)。

自適應自旋鎖意味著自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖物件上,自旋等待剛剛成功獲得過鎖,並且持有鎖的執行緒正在執行中,那麼虛擬機器就會認為這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞執行緒,避免浪費處理器資源。

八、總結

偏向鎖通過對比 Mark Word 解決加鎖問題,避免執行CAS操作。

輕量級鎖是通過用 CAS 操作和自旋來解決加鎖問題,避免執行緒阻塞和喚醒而影響效能。

重量級鎖是將除了擁有鎖的執行緒以外的執行緒都阻塞。

原文地址:www.jetchen.cn/synchronize…

synchronized四種鎖狀態的升級

參考:tech.meituan.com/2018/11/15/…

相關文章