synchronized四種鎖狀態的升級——多線程面試必問!

一、背景

在 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,關閉之後程序默認會進入輕量級鎖狀態。

偏向鎖升級:一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味着,它現在認爲只可能有一個線程來訪問它,所以當第一個線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭MarkWord成爲偏向鎖的時候使用CAS操作,並將對象頭中的ThreadID改成自己的ID,之後再次訪問這個對象時,只需要對比ID,不需要再使用CAS在進行操作。一旦有第二個線程訪問這個對象,因爲偏向鎖不會主動釋放,所以第二個線程可以看到對象時偏向狀態,這時表明在這個對象上已經存在競爭了,操作系統檢查原來持有該對象鎖的線程是否依然存活,如果掛了,則可以將對象變爲無鎖狀態,然後重新偏向新的線程,如果原來的線程依然存活,則馬上執行那個線程的操作棧,檢查該對象的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級爲輕量級鎖(偏向鎖就是這個時候升級爲輕量級鎖的)。如果不存在使用了,則可以將對象回覆成無鎖狀態,然後重新偏向。

五、輕量級鎖

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

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

在代碼進入同步塊的時候,如果同步對象鎖狀態爲無鎖狀態,虛擬機將首先在當前線程的棧幀中建立一個名爲鎖記錄(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 操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。

重量級鎖是將除了擁有鎖的線程以外的線程都阻塞。

 

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度 如果始終得不到索競爭的線程,使用自旋會消耗CPU 追求響應速度,同步塊執行速度非常快
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊執行速度較慢


作者:goldenJetty
鏈接:https://www.jianshu.com/p/d61f294ac1a6
來源:簡書,有所改動!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章