Java鎖升級

基礎知識之一:鎖的類型

鎖從宏觀上分類,分爲悲觀鎖與樂觀鎖。

樂觀鎖

樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。

java中的樂觀鎖基本都是通過CAS操作實現的,CAS是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。

悲觀鎖

悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。

基礎知識之二:java線程阻塞的代價

java的線程是映射到操作系統原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統介入,需要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,因爲用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態需要傳遞給許多變量、參數給內核,內核也需要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工作。

1.如果線程狀態切換是一個高頻操作時,這將會消耗很多CPU處理時間;
2.如果對於那些需要同步的簡單的代碼塊,獲取鎖掛起操作消耗的時間比用戶代碼執行的時間還要長,這種同步策略顯然非常糟糕的。

synchronized會導致爭用不到鎖的線程進入阻塞狀態,所以說它是java語言中一個重量級的同步操縱,被稱爲重量級鎖,爲了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,默認啓用了自旋鎖,他們都屬於樂觀鎖

明確java線程切換的代價,是理解java中各種鎖的優缺點的基礎之一。

markword

在介紹java鎖之前,先說下什麼是markword,markword是java對象數據結構中的一部分,要詳細瞭解java對象的結構可以點擊這裏,這裏只做markword的詳細介紹,因爲對象的markword和java各種類型的鎖密切相關;

markword數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,如下表所示:

狀態 標誌位 存儲內容 特點
未鎖定 01 對象哈希碼、對象分代年齡
輕量級鎖定 00 指向鎖記錄的指針 競爭線程數量少,鎖持有時間短,能通過自旋獲取鎖
膨脹(重量級鎖定) 10 執行重量級鎖定的指針 競爭線程數量多,鎖持有時間長
GC標記 11 空(不需要記錄信息)
可偏向 01 偏向線程ID、偏向時間戳、對象分代年齡 只有一個線程在重複請求鎖

32位虛擬機在不同狀態下markword結構如下圖所示:
在這裏插入圖片描述
前面提到了java的4種鎖,他們分別是重量級鎖、自旋鎖、輕量級鎖和偏向鎖,
不同的鎖有不同特點,每種鎖只有在其特定的場景下,纔會有出色的表現,java中沒有哪種鎖能夠在所有情況下都能有出色的效率,引入這麼多鎖的原因就是爲了應對不同的情況;

前面講到了重量級鎖是悲觀鎖的一種,自旋鎖、輕量級鎖與偏向鎖屬於樂觀鎖,所以現在你就能夠大致理解了他們的適用範圍,但是具體如何使用這幾種鎖呢,就要看後面的具體分析他們的特性;

輕量級鎖

加鎖過程:發現MarkWord已經偏向其他線程,並且鎖還沒有釋放(持有偏向鎖的線程處於同步塊?),此時把偏向鎖升級成輕量級鎖。在線程的棧幀裏創建一條鎖記錄(LockRecord),把MarkWord拷貝進去(DisplayedMarkWord),然後修改MarkWord指向鎖記錄。接着自旋。如果自旋之後還不能獲取鎖,則膨脹成重量級鎖,即修改MarkWord指向monitor對象(互斥變量?)。

輕量級鎖的加鎖過程:發現MarkWord已經偏向其他線程,但是鎖已經釋放,則撤銷偏向鎖,把MarkWord修改成無鎖,接着獲取偏向鎖。(疑問:偏向鎖不能自己解鎖?)

輕量級鎖的解鎖過程:通過CAS操作把MarkWord改寫成DisplacedMarkWord,也就是恢復到偏向鎖。如果修改失敗則說明已經是重量級鎖了。

Monitor的同步隊列包括:

競爭隊列ContentionList: 等待鎖的所有線程。是一個雙向列表,新線程(併發)通過CAS操作放入頭部,鎖的Owner(單線程)從尾部取線程放入EntryList
候選隊列EntryList: Owner把一個線程移入OnDeck
OnDeck: 同一時刻只有一個線程在競爭鎖
Owner: 持有鎖的線程
WaitSet: 調用了wait()的線程

偏向鎖升級成輕量級鎖

  • 線程A請求鎖,發現對象的MarkWord是無鎖狀態,嘗試CAS設置爲偏向鎖狀態,並寫入線程A的ID
  • 線程B也來請求鎖,發現MarkWord已經是偏向鎖狀態,檢查線程A是否存在
  • 如果此時線程A已經不存在
    • 將MarkWord設置爲無鎖狀態(?)
    • 嘗試CAS設置爲偏向鎖狀態,並寫入線程B的ID
  • 如果此時線程A存在
    • 暫停線程A
    • 在線程A的棧幀中創建鎖記錄(Lock Record)
    • 將MarkWord複製到該鎖記錄中
    • 嘗試CAS更新MarkWord,指向該鎖記錄
    • 更新鎖記錄的Owner指向MarkWord(?)
    • 設置MarkWord爲輕量級鎖狀態
    • 此時MarkWord與DisplacedMarkWord存儲了相同的內容(?)
    • 繼續執行線程A
    • 線程B自旋來獲取鎖

輕量級鎖膨脹成重量級鎖

  • 線程A棧幀的鎖記錄已經複製了MarkWord,並且MarkWord指向了該鎖記錄
  • 線程B來請求鎖,發現MarkWord已經是輕量級鎖,嘗試自旋(?)
  • 線程B自旋之後還是獲取不到鎖(?)
    • 更新MarkWord,指向重量級鎖(Mutex Lock)(?)
    • 設置MarkWord爲重量級鎖狀態
    • 阻塞線程B
  • 線程A嘗試CAS用DisplacedMarkWord替換當前的MarkWord,CAS失敗
    • 釋放鎖
    • 喚醒阻塞的線程

作者:金舜徑
鏈接:https://www.jianshu.com/p/c2089d096552
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發佈了158 篇原創文章 · 獲贊 72 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章