偏向鎖狀態轉移原理

爬蜥發表於2019-01-19

為什麼需要偏向鎖

當多個處理器同時處理的時候,通常需要處理互斥的問題。
一般的解決方式都會包含acquirerelease這個兩種操作,操作保證,一個執行緒在acquire執行之後,在它執行release之前,其它執行緒不能完成acquire操作。這個過程經常就涉及到鎖。研究表明(L. Lamport A fast mutual execlusion algorithm),通過 fast locks演算法可以做到,lock和unlock操作所需的時間與潛在的競爭處理器數無關。
java內建了monitor來處理多執行緒競爭的情況.

  1. 一種優化方式是使用 輕量鎖來在大多數情況下避免重量鎖的使用,輕量鎖的主要機制是在monitor entry的時候使用原子操作,某些退出操作也是這樣,如果有競爭發生就轉而退避到使用作業系統的互斥量

    輕量鎖認為大多數情況下都不會產生競爭

在鎖的使用中一般會使用幾種原子指令:
- CAS:檢查給定指標位置的值和傳入的值是否一致,如果一致,就修改
- SWAP:替換指標原位置的值,並返回舊的值
- membar:記憶體屏障約束了處理器在處理指令時的重排序情況,比如禁止同讀操作被重排序到寫操作之後

Java中使用 two-word 物件頭
1. 是 mark word,它包括同步資訊,垃圾回收資訊、hash code資訊
2. 指向物件的指標物件

這些指令的花銷很昂貴,因為他們的實現通常會耗盡處理器的重排序緩衝區,從而限制了處理器原本能夠像流水線一樣處理指令的能力。研究資料發現(Eliminating_synchronization-related_atomic_operations_with_biased_locking_and_bulk_rebiasing)原子操作在真實的應用中,比如javac ,會導致效能下降20%。

> [此處2006年的文章第4段](https://blogs.oracle.com/dave/biased-locking-in-hotspot)大概說CAS和fence在作業系統中是序列化處理的,而序列化指令會使CPU幾乎停止,終止並禁止任何無需指令,並等待本地儲存耗盡。在多核處理器上,這種處理會導致相當大的效能損失
  1. 另一種優化的方式是使用偏向鎖,它不僅認為大多數情況下是沒有競爭的,而且在整個的monitor的一生中,都只會有一個執行緒來執行enter和exit,這樣的監視器就很適合偏向於這個執行緒了。當然如果這時有另外一個執行緒嘗試進入偏向鎖,即使沒有發生競爭,也需要執行 偏向鎖撤銷操作

輕量鎖

  1. 當輕量鎖通過monitorenter指令獲取鎖的時候,鎖記錄肯定會被記錄到執行緒的棧裡面去,以表示鎖獲取操作。鎖記錄會持有原始物件的mark word和一些必備的後設資料來識別鎖住的物件。在獲取鎖的時候,mark word會被拷貝一份到鎖記錄(這個操作稱為 displaced mark word)然後執行CAS操作嘗試是的物件的mark word指標指向鎖記錄。如果CAS成功,當前執行緒就持有了鎖,如果失敗,其它執行緒獲取鎖,這是鎖就“膨脹”,轉而使用了作業系統的互斥量和條件,在“膨脹”的過程中,物件本身的mark word會經過CAS操作指向含有mutex和condition的資料結構。
  2. 當執行unlock的時候,扔通過CAS來操作mark word,如果CAS成功了,說明沒有競爭,同時維持輕量鎖;如果失敗了,鎖就處於競爭態,當被持有時,會以一種“非常慢”的方式來正確的釋放鎖並通知其他等待執行緒來獲取鎖
  3. 同一個執行緒重新處理的方式很直白,在輕量鎖發現要獲取的鎖已經被當前執行緒持有的時候,它會存一個0進去,而不對mark word做任何處理,同樣在unlock的時候,如果有看到0,也不會更新物件的mark word.並每次重入,都會明確的記錄count。

偏向鎖的實現

圖片描述

執行緒指標是NULL(0)表示當前沒有執行緒被偏向這個物件

當分配一個物件並且這個物件能夠執行偏向的時候並且還沒有偏向時,會執行CAS是的當前執行緒ID放入到mark word的執行緒ID區域。

  1. 如果成功,物件本身就會被偏向到當前執行緒,當前執行緒會成為偏向所有者

    執行緒ID直接指向JVM內部表示的執行緒;java虛擬機器中則是在最後3bit填充0x5表示偏向模式。

  2. 如果CAS失敗了,即另一個執行緒已經成為偏向的所有者,這意味著這個執行緒的偏向必須撤銷。物件的狀態會變成輕量鎖的模式,為了達到這一點,嘗試把物件偏向於自己的執行緒必須能夠操作偏向所有者的棧,為此需要全域性安全點已經觸達(沒有執行緒在執行位元組碼)。此時偏向擁有者會像輕量級鎖操作那樣,它的堆疊會填入鎖記錄,然後物件本身的mark word會被更新成指向棧上最老的鎖記錄,然後執行緒本身在安全點的阻塞會被釋放

    如果沒有被原有的偏向鎖持有者持有,會撤銷物件重新回到可偏向但是還沒有偏向的狀態,然後嘗試重新獲取鎖。如果物件當前鎖住了是進入輕量鎖,如果沒有鎖住是進入未被鎖定的,不可偏向物件

下一個獲取鎖的操作會與檢測物件的mark word,如果物件是可偏向的,並且偏向的所有者是當前那執行緒,會沒有任何額外操作而立馬獲取鎖。

這個時候偏向鎖的持有者的棧不會初始化鎖記錄,因為物件偏向的時候,是永遠不會檢驗鎖記錄的

unlock的時候,會測試mark word的狀態,看是否仍然有偏向模式。如果有,就不會再做其它的測試,甚至不需要管執行緒ID是不是當前執行緒ID

這裡通過直譯器的保證monitorexit操作只會在當前執行緒執行,所以這也是一個不需要檢查的理由

不適用偏向鎖的模式

  1. 生產生-消費者模式,會有過個執行緒參與競爭;
  2. 一個執行緒分配多個物件,然後給每個物件執行初始的同步操作,再有其它執行緒來處理子流程

批量回到可偏向狀態還是撤銷可偏向?

經驗發現為特定的資料結構選擇性的禁用偏向鎖(Store-fremm biased lock SFBL)來避免不合適的情況是合理的。為此需要考慮每個資料結構到底是執行撤銷偏向的消耗小還是重新回到可偏向的狀態消耗下。一種啟發式的方式來決定到底是執行那種方式,在每個類的後設資料裡面都會包含一個counter和時間戳,每次偏向鎖的例項執行一次偏向撤銷,都會自增,時間戳用於記錄上次執行bulk rebias的時間。

撤銷計數並統計那些處於可偏向但是未偏向狀態的撤銷,這些操作的撤銷只需要一次CAS就可以

counter本身有兩個閾值,一個是bulk rebias閾值,一個是bulk revocation。剛開始的時候,這種啟發式的演算法可以單獨的決定執行rebias還是revoke,一單bulk rebias的閾值達到,就會執行bulk rebias,轉移到 rebiasable狀態
time閾值用來重置撤銷的計數counter,如果自從上次執行bulk bias已經超過了這個閾值時間,就會發生counter的重置。

這意味著從上次執行bulk rebias到現在並沒有執行多次的撤銷操作,也就是說執行bias仍然是個不錯的選擇

但是如果在執行了bulk rebias之後,在時間閾值之內,仍然一直有撤銷數量增長,一旦達到了bulk revocation的閾值,就會執行bulk revocation,此時這個類的物件不會再被允許使用偏向鎖。

Hotspot中的閾值如下 Bulk rebias threshold 20 Bulk revoke threshold 40 Decay time 25s

撤銷偏向本身是一個消耗很大的事情,因為它必須掛起執行緒,遍歷棧找到並修改lock records(鎖記錄)

最明顯的查詢某個資料結構的所有物件例項的方式就是遍歷堆,這種方式在堆比較小的時候還可以,但是堆變大就顯得效能不好。為類解決這個為題,使用 epoch
epoch是一個時間戳,用來表明偏向的合法性,只要這個資料介面是可偏向的,那麼就會在mark word上有一個對應的epoch bit位

這個時候,一個物件被認為已經偏向了執行緒T必須滿足兩個條件,1: mark word中偏向所有這的標記必須是這個執行緒,2:例項的epoch必須是和資料結構的epoch相等
epoch本身的大小是限制的,也就是有可能出現迴圈,但這並不影響方案的正確性

通過這種方式,類C的bulk rebiasing操作會少去很多的花銷。具體操作如下

  1. 增大類C的epoch,它本身是一個固定長度的integer,和物件頭中的epoch擁有一樣的bit位數
  2. 掃描所有的執行緒棧來定位當前類C的例項中已經鎖住的,更新他們的epoch為類C的新的epoch或者是,根據啟發式策略撤銷偏向

這樣就不用掃描堆了,對於那些沒有被改變epoch的例項(和類的epoch不同),會被自動當做可偏向但是還沒有偏向的狀態

這種狀態可看做 rebiaseable

膨脹與偏向原始碼

當前HotSpot虛擬機器的實現

批量撤銷本身存在著效能問題,一般的解決方式如下

  1. 新增epoch,如前所訴
  2. 執行緒第一次獲取的時候不偏向,而是在執行一定數量後都有同一個執行緒獲取再偏向
  3. 允許鎖具有永遠改變(或者很少)的固定偏向執行緒,並且允許非偏向執行緒獲取鎖而不是撤銷鎖。

    這種方式必須確保獲取鎖的執行緒必須確保進去臨界區之前沒有其它執行緒持有鎖,並且不能使用 read-modify-write的指令,只能使用read和write

當前Hotspot JVM中的在32位和64位有不同的形式
64bit為

圖片描述
32bit為

圖片描述

輕量鎖(thin locks),細節如前所述。它在HotSpot中使用displaced header的方式實現,又被稱作棧鎖

mark完整的狀態轉換關係如下

圖片描述

  1. 剛分配物件,此時物件是可偏向並且未偏向的
  2. 物件偏向於執行緒T,並記下epoch
  3. 此時有新執行緒來競爭

    • 3.1一種策略是T執行對應的unlock,並重新分配給新的執行緒,以便不需要執行撤銷操作
    • 3.2 如果已經偏向的物件被其它執行緒通過wait或者notify操作了,裡面進入膨脹裝態,使用重量鎖
  4. 此時有新的執行緒來競爭,一種策略是使用啟發式的方式來統計撤銷的次數

    • 4.1 當撤銷達到bulk rebias的閾值時,執行bulk rebias
    • 4.2 當撤銷達到bulk revoke,並且此時所仍然被持有(原偏向鎖持有者),轉向輕量鎖(hashcode的計算依賴於膨脹來支援修改displaced mark word)
    • 4.3 當撤銷達到bulk revoke,並且此時所沒有被持有(原偏向鎖持有者),轉向未被鎖定不可偏向的狀態,此時沒有進行hashcode計算
  5. 對於經過bulk rebias的物件,檢查期間沒有鎖定的例項,它的epoch會和class的不一樣,變成過期,但是可以偏向

    • 5.1 如果 發生垃圾回收,lock會被初始化成可偏向但未偏向的狀態(這也可以降低epoch迴圈使用的影響)

      • 5.2 如果重新被執行緒獲取偏向鎖,回到偏向鎖獲取狀態
  6. 處於輕量鎖狀態,它可能沒有hashcode計算,可能有,這依賴於inflat

    • 6.1 沒有hashcode,此時解鎖回到沒有hashcode計算的不可偏向的狀態
    • 6.2 又被其它執行緒佔有,轉移到重量鎖(比如使用POXIS作業系統的mutex和condition)
  7. 未被鎖定不可偏向的狀態同時沒有hashcode計算加鎖後轉移到輕量鎖
  8. 處於重量鎖狀態

    • 8.1 8.2 如果在Stop-The-Word期間沒有競爭了,就可以去膨脹(STW期間沒有其它執行緒獲取和釋放鎖,是安全的),根據是否有hashcode,退到對應的狀態(就是就退回使用偏向鎖 )
    • 8.3 重量鎖期間的lock/unlock仍然處於重量鎖
  9. 計算過hashcode,再加鎖和解鎖對應狀態轉換(9.10)

    附錄

    Quickly Reacquirable Locks Dave Dice Mark Moir Bill Scherer

Eliminating_synchronization-related_atomic_operations_with_biased_locking_and_bulk_rebiasing

Evaluating and improving biased locking in the HotSpot virtual machine
biased-locking-in-hotspot

相關文章