偏向鎖狀態轉移原理

爲何須要偏向鎖

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

  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,若是對象是可偏向的,而且偏向的全部者是當前那線程,會沒有任何額外操做而立馬獲取鎖。oracle

這個時候偏向鎖的持有者的棧不會初始化鎖記錄,由於對象偏向的時候,是永遠不會檢驗鎖記錄的

unlock的時候,會測試mark word的狀態,看是否仍然有偏向模式。若是有,就不會再作其它的測試,甚至不須要管線程ID是否是當前線程IDapp

這裏經過解釋器的保證monitorexit操做只會在當前線程執行,因此這也是一個不須要檢查的理由

不適用偏向鎖的模式

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

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

經驗發現爲特定的數據結構選擇性的禁用偏向鎖(Store-fremm biased lock SFBL)來避免不合適的狀況是合理的。爲此須要考慮每一個數據結構究竟是執行撤銷偏向的消耗小仍是從新回到可偏向的狀態消耗下。一種啓發式的方式來決定究竟是執行那種方式,在每一個類的元數據裏面都會包含一個counter和時間戳,每次偏向鎖的實例執行一次偏向撤銷,都會自增,時間戳用於記錄上次執行bulk rebias的時間。post

撤銷計數並統計那些處於可偏向可是未偏向狀態的撤銷,這些操做的撤銷只須要一次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

相關文章
相關標籤/搜索