Java 多線程 - 鎖優化(輕量級鎖、偏向鎖原理及鎖的狀態流轉)

前言

爲了進一步改進高效併發,HotSpot虛擬機開發團隊在JDK1.6版本上花費了大量精力實現各種鎖優化。如適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等。(主要指的是synchronized的優化)。

適應性自旋 (自旋鎖)

爲了讓線程等待,我們只需要讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。引入自旋鎖的原因是互斥同步對性能最大的影響是阻塞的實現,管錢線程和恢復線程的操作都需要轉入內核態中完成,給併發帶來很大壓力。自旋鎖讓物理機器有一個以上的處理器的時候,能讓兩個或以上的線程同時並行執行。我們就可以讓後面請求鎖的那個線程**“稍等一下”**,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖

自旋鎖雖然能避免進入阻塞狀態從而減少開銷,但是它需要進行忙循環操作佔用 CPU 時間,它只適用於共享數據的鎖定狀態很短的場景

在 JDK 1.6之前,自旋次數默認是10次,用戶可以使用參數-XX:PreBlockSpin來更改。

JDK1.6引入了自適應的自旋鎖。自適應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。(這個應該屬於試探性的算法)。

鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行清除。鎖清除的主要判定依據來源於逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程訪問到,那就可以把它們當做棧上數據對待,認爲它們是線程私有的,同步枷鎖自然就無需進行

簡單來說,Java 中使用同步 來保證數據的安全性,但是對於一些明顯不會產生競爭的情況下,Jvm會根據現實執行情況對代碼進行鎖消除以提高執行效率。

舉例說明

對於一些看起來沒有加鎖的代碼,其實隱式的加了很多鎖,這些也是鎖消除優化的對象。例如下面的字符串拼接代碼就隱式加了鎖:

String 是一個不可變的類,編譯器會對 String 的拼接自動優化。在 JDK 1.5 之前,會轉化爲StringBuffer對象的連續 append()操作:

每個 append() 方法中都有一個同步塊。虛擬機觀察變量 sb,很快就會發現它的動態作用域被限制在 concatString()方法內部。也就是說,sb 的所有引用永遠不會逃逸到concatString()方法之外,其他線程無法訪問到它,因此可以進行消除。

鎖粗化

  • 如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,頻繁的加鎖操作就會導致性能損耗。
  • 當多個彼此靠近的同步塊可以合併到一起,形成一個同步塊的時候,就會進行鎖粗化。該方法還有一種變體,可以把多個同步方法合併爲一個方法。如果所有方法都用一個鎖對象,就可以嘗試這種方法。

輕量級鎖 (@重點知識點)

JDK 1.6 引入了偏向鎖和輕量級鎖,從而讓鎖擁有了四個狀態:無鎖狀態(unlocked)、偏向鎖狀態(biasble)、輕量級鎖狀態(lightweight locked)和重量級鎖狀態(inflated)

重量級排序 :重量級鎖 > 輕量級鎖 > 偏向鎖 > 無鎖

先介紹一下HotSpot 虛擬機對象頭的內存佈局:

上面這些數據被稱爲Mark Word - 標記關鍵詞。 其中 tag bits 對應了五個狀態,這些狀態的含義在右側的 state 表格中給出。除了 marked for gc 狀態(gc標記狀態),其它四個狀態已經在前面介紹過了。

下圖左側是一個線程的虛擬機棧,其中有一部分稱爲 Lock Record 的區域,這是在輕量級鎖運行過程創建的,用於存放鎖對象的 Mark Word。而右側就是一個鎖對象,包含了 Mark Word 和其它信息

簡單來講,輕量鎖就是先通過CAS操作進行同步,因爲絕大部分的鎖,在整個同步週期都是不存在線程去競爭的。

獲取輕量鎖過程當中會當前線程的虛擬機棧中創建一個Lock Record的內存區域去存儲獲取鎖的記錄(類似於操作記錄?),然後使用CAS操作將鎖對象的Mark Word更新成指向剛剛創建的Lock Record的內存區域的指針,如果這個操作成功,就說明線程獲取了該對象的鎖,把對象的Mark Word 標記00,表示該對象處於輕量級鎖狀態。失敗情況就如上所述,會判斷是否是該線程之前已經獲取到鎖對象了,如果是就進入同步塊執行。如果不是,那就是有多個線程競爭這個所對象,那輕量鎖就不適用於這個情況了,要膨脹成重量級鎖。

下圖是對象處於輕量級鎖的狀態。

偏向鎖 (@重點知識點)

偏向鎖的思想是偏向於讓第一個獲取鎖對象的線程,這個線程在之後獲取該鎖就不再需要進行同步操作,甚至連 CAS 操作也不再需要

當鎖對象第一次被線程獲得的時候,進入偏向狀態,標記爲 |1|01|(前面內存佈局圖中說明了,這屬於偏向鎖狀態)。同時使用 CAS 操作將線程 ID (ThreadID)記錄到 Mark Word 中,如果 CAS 操作成功,這個線程以後每次進入這個鎖相關的同步塊就不需要再進行任何同步操作

當有另外一個線程去嘗試獲取這個鎖對象時,偏向狀態就宣告結束,此時撤銷偏向(Revoke Bias)後恢復到未鎖定狀態或者輕量級鎖狀態。

引用《阿里手冊:碼出高效》的描述再理解一次:

  • 偏向鎖是爲了在資源沒有被多線程競爭的情況下儘量減少鎖帶來的性能開銷。
  • 在鎖對象的對象頭中有一個ThreadId字段,當第一個線程訪問鎖時,如果該鎖沒有被其他線程訪問過,即**ThreadId字段爲空**,那麼JVM讓其持有偏向鎖,並將ThreadId字段的值設置爲該線程的ID。當下一次獲取鎖的時候,會判斷ThreadId是否相等,如果一致就不會重複獲取鎖,從而提高了運行效率。
  • 如果存在鎖的競爭情況,偏向鎖就會被撤銷並升級爲輕量級鎖。

可以結合下面這張鎖的狀態流轉圖理解一下:

上圖實際上是摘自《深入理解Java虛擬機》,自己重新畫了一次。在畫圖的過程當中,發現圖中有兩個點不是很理解,書中也沒有對應的解釋。就是偏向鎖的重偏向撤銷偏向時如果判斷對象是否已經鎖定

後面經過一段時間的查詢才知道,HotSpot支持存儲釋放偏向鎖,以及偏向鎖的批量重偏向和撤銷。這個特性可以通過JVM的參數進行切換,而且這是默認支持的。

Unlock狀態下Mark Word的一個比特位用於標識該對象偏向鎖是否被使用或者是否被禁止。如果該bit位爲0,則該對象未被鎖定,並且禁止偏向;如果該bit位爲1,則意味着該對象處於以下三種狀態:

  • 匿名偏向(Anonymously biased)
    在此狀態下thread pointerNULL(0),意味着還沒有線程偏向於這個鎖對象。第一個試圖獲取該鎖的線程將會面臨這個情況,使用原子CAS指令可將該鎖對象綁定於當前線程。這是允許偏向鎖的類對象的初始狀態。
  • 可重偏向(Rebiasable)
    在此狀態下,偏向鎖的epoch字段是無效的(與鎖對象對應的class的mark_prototype的epoch值不匹配)。下一個試圖獲取鎖對象的線程將會面臨這個情況,使用原子CAS指令可將該鎖對象綁定於當前線程**。在批量重偏向的操作中,未被持有的鎖對象都被至於這個狀態,以便允許被快速重偏向**。
  • 已偏向(Biased)
    這種狀態下,thread pointer非空,且epoch爲有效值——意味着其他線程正在持有這個鎖對象。

這部分因爲我目前暫時不想鑽研這麼深,就簡單描述了一下狀態流轉機制,就當給自己留個坑先記錄一下。想要更深的理解知識的話請需要參考下面的文章(使用關鍵詞"bias revocation"進行搜索觀看,第二篇寫的很好,之後肯定要全篇好好拜讀):

  1. Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing (這個是pdf版ppt文件,需要翻牆哦)

  2. Evaluating and improving biased locking in the HotSpot Virtual Machine(這篇我還查到了中文翻譯,只不過只翻譯了一點,也不保證翻譯質量,看原文實際上更好點,講的很透徹,詳細講了JVM偏向鎖的機制,原理,批量重偏向、撤銷偏向的操作,相關章節就在下方截圖)

StackOverflow上關於這個議題還有一個很有意思的問題,有興趣的可以去看看。Does Java ever rebias an individual lock

通俗點總結

  • 偏向鎖是適用於很長一段時間(抽象意義上)都是只有一個線程進入臨界區的情況,使用ThreadId進行標記;

  • 輕量鎖是適用於會較輕的鎖競爭情況,多個線程交替進入臨界區,使用CAS獲取鎖對象;

  • 重量級鎖(互斥同步)適用於較嚴重的鎖競爭情況,多個線程同時進入臨界區,這個情況下多個線程如果是等待輕量級鎖的話,就需要多個線程一直自旋,CPU時間會損失很多;

參考

  1. 《深入理解Java虛擬機》
  2. 《碼出高效》
  3. The Hotspot Java Virtual Machine
  4. Biased Locking in HotSpot
  5. Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing
  6. Evaluating and improving biased locking in the HotSpot Virtual Machine
  7. Does Java ever rebias an individual lock
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章