從 Synchronized 到鎖的優化

我們知道 SynchronizedJava 中解決併發問題的一種最常用的方法, 也是最簡單的一種方法. 被也被稱爲內置鎖.

Synchronized 的作用主要有三個:

  • 確保線程互斥的訪問同步代碼
  • 保證共享變量的修改能夠及時可見
  • 有效解決重排序問題。

 
從語法上講, Synchronized 總共有三種用法:

  • 修飾普通方法, 鎖是當前實例對象.
  • 修飾靜態方法, 鎖是當前類的 class 對象.
  • 修飾代碼塊, 鎖是括號中的對象.

關於使用方式, 這裏就不再進行一一描述了. 我們直接進入正題, 看 Synchronized 的底層實現原理是什麼.

1. Synchronized 原理

首先, 我們先來看一段代碼, 使用了同步代碼塊和同步方法, 通過使用 javap 工具查看生成的 class 文件信息來分析 synchronized 關鍵字的實現細節.


對代碼進行反編譯後的結果如下

  public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic    
         3: dup
         4: astore_1
         5: monitorenter  //---------------------------------------------1.
         6: aload_1
         7: monitorexit    //---------------------------------------------2.
         8: goto          16
        11: astore_2
        12: aload_1
        13: monitorexit   //---------------------------------------------3.
        14: aload_2
        15: athrow
        16: return
        ...

  public static synchronized void test();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //---------------------------------------------4.
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 21: 0

從生產的 class 信息中, 可以清楚的看到兩部分內容

  • 同步代碼塊中使用了 monitorentermonitorexit 指令.
  • 同步方法中依靠方法修飾符 flags 上的 ACC_SYNCHRONIZED 實現.

先看反編譯出 main 方法中標記的 1 與 2. monitorenter / monitorexit 關於這兩條指令的作用, 參考 JVM 中對他們的描述如下:

monitorenter
每個對象有一個監視器鎖 monitor, 當 monitor 被佔用時就會處於鎖定狀態, 線程執行 monitorenter 指令時嘗試獲取 monitor 的所有權, 過程如下

  • 如果 monitor 的進入數爲 0 , 則該線程進入 monitor, 然後將進入數設置爲 1, 該線程即爲 monitor 的擁有者.
  • 如果線程已經佔有該 monitor, 只是重新進入, 則進入 monitor 的進入數加 1.
  • 如果其他線程已經佔用了 monitor, 則該線程進入阻塞狀態, 直到 monitor 的進入數爲 0, 再嘗試獲取 monitor 的所有權.

 
monitorexit
執行 monitorexit 的線程必須是對應 monitor的所有者. 執行指令時, monitor的進入數減 1. 如果減 1 後進入數爲 0, 則線程退出 monitor. 不再是這個 monitor 的所有者. 其他被這個 monitor 阻塞的線程可以嘗試去獲取這個 monitor 的所有權.

 

monitorenter 指令是在編譯後插入到同步代碼塊開始的位置, 而 monitorexit 是插入到方法的結束處和異常處. 這也就是爲什麼在 3 處會單獨有一個 monitorexit 了.

 

ACC_SYNCHRONIZED
當方法調用時, 調用指令將檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置, 如果設置了, 執行線程將先獲取 monitor, 獲取成功之後才能執行方法體. 方法執行完後再釋放 monitor, 在方法執行期間, 其他任何線程都無法再獲得同一個 monitor 對象.
其實這個和上面 monitorentermonitorexit 本質上沒有區別, 只是方法的同步是一種隱式的方式來實現的, 無需通過字節碼來完成.

看完這些, 是不是覺得有點和 AQS 中的 state 相似? 如果看完了 從 LockSupport 到 AQS 的簡單學習 這篇文章的朋友, 再來看這裏, 我相信應該會很容易理解.

這裏既然說到了監視器鎖 monitor , 我們一起來看這到底是什麼.
 


2. 監視器鎖 monitor

監視器鎖 monitor 本質是依賴於底層操作系統的 Mutex Lock (互斥鎖) 來實現的. 每個對象都對應於一個可稱爲 "互斥鎖" 的標記, 這個標記用來保證在任一時刻, 只能有一個線程訪問該對象.

以下是 Mutex 的工作方式

  • 申請 Mutex.
  • 如果成功, 則持有該 Mutex.
  • 如果失敗, 則進行自旋, 自旋的過程就是在等待 Mutex, 不斷髮起 Mutex gets, 直到獲取 Mutex 或者達到自旋次數的限制爲止.
  • 依據工作模式的不同選擇 yiled 還是 sleep.
  • 若達到 sleep 限制或者主動喚醒,又或者完成 yiled, 則繼續重複上面 4 點, 直到獲得 Mutex 爲止.

 

3. 爲什麼說 Synchronized 是重量級鎖?

Synchronized 是通過對象內部的一個叫監視器鎖 monitor 來實現的, 監視器鎖本質又是依賴於底層的操作系統的互斥鎖 Mutex Lock 來實現的.

而從 Mutex Lock (互斥鎖) 的工作流程我們可以得知是自旋和阻塞, 既然是阻塞那麼肯定有喚醒. 由於 Java 的線程是映射到操作系統的原生線程之上的, 所以說如果要阻塞或者喚醒一條線程, 都需要操作系統來幫忙完成, 這就需要從用戶態轉到內核態. 這個成本非常高, 狀態之間的轉換需要相對較長的時間, 因此狀態轉換需要消耗很多處理器時間. 這就是爲什麼 Synchronized 效率低的原因. 因此, 這種依賴於操作系統互斥鎖 Mutex Lock 所實現的鎖, 我們稱之爲 "重量級鎖".

But, 在 JDK1.6 中爲了獲得鎖和釋放鎖帶來的性能消耗, 引入了 偏向鎖輕量級鎖, 使得 SynchronizedReentrantLock 的性能基本持平. ReentrantLock 只是提供了比 Synchronized 更豐富的功能, (比如嘗試獲取鎖,嘗試釋放鎖等) 而不一定有更優的性能, 所以在 Synchronized 能實現需求的情況下, 儘量還是優先考慮使用 Synchronized 來進行同步.

鎖一共有 4 種狀態: 級別從低到高依次是: 無鎖狀態 -> 偏向鎖狀態 -> 輕量級鎖狀態 -> 重量級鎖狀態
鎖可以升級, 但是不能降級.

在 JDK1.6 中, 除了引入偏向鎖與輕量級鎖的概念, 還有鎖消除, 鎖粗化等等.

接下來我們瞭解鎖是如何優化前, 需要先了解一個重要的概念, 那就是 java 對象頭.

 

4. java 對象頭

Synchronized 鎖是存在 java 對象頭中的, 那麼什麼是 java 對象頭呢?
Hotspot 虛擬機中, 對象在內存的分佈爲三個部分, 頭像頭, 實例數據, 對齊填充. 而對象頭主要包括

  • Mark Word (標記字段) : 用於存儲對象自身的運行時數據, 如哈希碼, GC分代年齡, 鎖狀態標誌, 線程持有的鎖, 偏向線程 ID, 時間戳等等.
  • Klass Pointer (類型指針) : 存儲對象的類型指針,該指針指向它的類元數據

Hotspot 虛擬機: JVM 虛擬機, 總的來說是一種標準規範, 虛擬機有很多實現版本. 主要作用就是運行 java 的類文件的. 而 Hotspot 虛擬機是虛擬機的一種實現, 它是 SUN 公司開發的, 是 sun jdk 和 open jdk 中自帶的虛擬機, 同時也是目前使用範圍最廣的虛擬機.

Hotspot 與 JVM 兩者的區別一個是實現方式, 一個是標準.

額外知識點 : Java 對象頭一般佔有 2 個機器碼(在 32 位虛擬機中, 1 個機器碼等於 4 字節, 也就是 32 bit), 但是如果對象是數組類型, 則需要 3 個機器碼, 因爲 JVM 虛擬機可以通過 Java 對象的元數據信息確定 Java 對象的大小, 但是無法從數組的元數據來確認數組的大小, 所以用一塊來記錄數組的長度. 下圖是Mark Word 默認的存儲結構 (32 位虛擬機)

對象頭信息是與對象自身定義的數據無關的額外存儲成本, 但是考慮到虛擬機的空間效率, Mark Work 被設計成一個非固定的數據結構, 以便在極小的空間內存儲更多的信息. 也就是說, Mark Word 會隨着程序的運行發生變化, 變化狀態如下(32 位虛擬機)

我們現在知道鎖的狀態及相關信息是存在了 java 對象頭中的 Mark Word 中. 接着來看下鎖是如何優化的. 無鎖狀態就不再說了, 我們從最低的偏向鎖開始.

 

5. 鎖優化 - 偏向鎖

什麼是偏向鎖
偏向鎖, 顧名思義, 它會偏向於第一個訪問鎖的線程. 如果在運行過程中只有一個線程訪問同步塊, 會在對象頭和棧幀中的鎖記錄裏存儲當前線程的 ID, 以後該線程在進入和退出同步塊時不需要進行 CAS 操作來加鎖和解鎖, 只需要簡單的判斷一下對象頭的 Mark Word 裏是否存儲着當前線程的ID.

 
爲什麼要引入偏向鎖
經過研究發現, 在大多數情況下, 鎖不僅不存在多線程競爭, 而且總是由同一線程多次獲得, 爲了讓線程獲得鎖的代價更低而引入了偏向鎖, 減少不必要的 CAS 操作, 從而提高性能.

引入偏向鎖是爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑, 因爲輕量級鎖的獲取和釋放依賴多次的 CAS 原子指令. 而偏向鎖只需要在置換線程 ID 的時候依賴一次 CAS 原子指令. 因爲一旦出現多線程競爭的情況就必須撤掉偏向鎖, 膨脹爲輕量級鎖. 所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的 CAS 原子指令的性能消耗.

 
偏向鎖的三種狀態

  • 匿名偏向: 這是允許偏向鎖的初始狀態, 其 Mark Word 中的 Thread ID 爲0, 第一個試圖獲取該對象鎖的線程會遇到這種狀態, 可以通過 CAS 操作修改 Thread ID 來獲取這個對象的鎖.
  • 可重偏向: 這個狀態下 Epoch 是無效的, 下一個線程會遇到這種情況, 在批量重偏向操作中, 所有未被線程持有的對象都會被設置成這個狀態. 然後在下個線程獲取的時候能夠重偏向. (批量重偏向這裏不深入分析, 有興趣的可以執行研究一下)
  • 已偏向: 這個狀態最簡單, 就是被線程持有着, 此時 Thread ID 爲其偏向的線程.

如果 JVM 啓用偏向鎖, 那麼一個新建未被任何線程獲取的對象頭 Mark Word 中的 Thread Id 爲0, 是可以偏向但未偏向任何線程, 被稱爲匿名偏向狀態. 而無鎖狀態是不可偏向也未偏向任何線程, 不可再變爲偏向鎖. 記住!無鎖狀態不能變成偏向鎖!

 
偏向鎖獲取過程

  1. 在一個線程進入同步塊的時候, 檢測 Mark Word 中是否爲可偏向狀態, 即 [是否是偏向鎖] = 1, [鎖標誌位] = 01
  2. 若爲可偏向狀態, 檢測 Mark Word中記錄的線程 ID 是否是當前線程 ID, 如果是執行步驟 5, 不是執行步驟 3. 若爲不可偏向狀態, 直接執行輕量級鎖流程.
  3. 如果線程ID並未指向當前線程,則通過 CAS 操作競爭鎖, 競爭成功則將 Mark Word 中線程 ID 設置爲當前線程 ID. 然後執行步驟 5, 否則執行步驟 4.
  4. 通過 CAS 獲取偏向鎖失敗, 則表示有競爭. (CAS 獲取偏向鎖失敗說明至少有過其他線程曾獲得過偏向鎖, 因爲線程不會主動釋放偏向鎖). 當到達全局安全點 (safepoint) 時, 會首先掛起擁有偏向鎖的線程, 然後檢查持有偏向鎖的線程是否還活着, (因爲有可能持有偏向鎖的線程已經執行完畢, 但是該線程不會主動釋放偏向鎖)
    • 如果還存活, 接着判斷是否還在同步代碼塊中執行.
      • 若還在同步代碼塊中執行, 直接升級爲輕量級鎖.
      • 若未在同步代碼塊中執行, 則看是否可重偏向,
        • 不可重偏向: 直接撤銷偏向鎖, 變爲無鎖狀態後, 升級爲輕量級鎖.
        • 可重偏向: 修改 Mark Word爲匿名偏向狀態, 通過 CAS 將新線程 ID給 Mark Word 賦值.喚醒新線程, 執行同步代碼塊.
    • 如果不存活, 也需要判斷是否可重偏向.
      • 不可重偏向: 直接撤銷偏向鎖, 變爲無鎖狀態後, 升級爲輕量級鎖.
      • 可重偏向: 修改 Mark Word爲匿名偏向狀態, 通過 CAS 將新線程 ID給 Mark Word 賦值.喚醒新線程, 執行同步代碼塊.

JVM 維護了一個集合存放所有存活的線程, 通過遍歷該集合判斷是否有線程的 ID 等於持有偏向鎖線程的 ID, 有的話表示存活.

 
至於是否還在同步塊中執行: 這個就需要說到鎖記錄 Lock Record

當代碼進入同步塊的時候, 如果此時同步對象未被鎖定 (即 [鎖標誌位] = 01) , 虛擬機會在當前線程的棧幀中新建一個空間, 來存放鎖記錄 Lock Record , 鎖記錄用於存儲記錄目前對象頭 Mark Word 的拷貝 (官方稱之爲 Displaced Mark Word) 以及記錄鎖對象的指針 owner.

棧幀: 這個概念涉及的內容較多, 不便於展開敘述. 從理解下文的角度上來講, 只需要知道, 每個線程都有自己獨立的內存空間, 棧幀就是其中的一部分. 裏面可以存儲僅屬於該線程的一些信息.

在偏向鎖時也有 Lock Record 存在, 只不過作用不大. Lock Record 主要用於輕量級鎖和重量級鎖.


 
鎖記錄可以做什麼?
可以統計重入的次數, 判斷當先線程是否還在同步塊中執行.以及在輕量級鎖中會大量用到.

統計重入次數
線程每次進入同步塊(即執行monitorenter)都會新建一個鎖記錄, 並將新建鎖記錄中的 Displaced Mark Word 設爲 null . 用來作爲統計重入的次數. owner 指向當前的鎖對象.

每次解鎖 (即執行monitorexit) 的時候都會從最低的一個相關的鎖記錄移除. 所以可以通過遍歷線程棧中的Lock Record來判斷線程是否還在同步塊中.

下圖是一個重入三次的 Lock Record 示意圖.

爲什麼 JVM 選擇在線程棧幀中添加 Displaced Mark WordnullLock Record 來表示重入計數而不是將重入次數直接放在對象頭的 Mark Word 中呢. 之前說過, Mark Word 的大小是有限制的, 已經存不下該信息了.

那麼爲什麼不只創建一個鎖記錄在其中記錄重入次數呢? 這點我也沒有想明白. 如果有知道答案的朋友, 請留言告知一下, 萬分感謝 !!!
 

 
偏向鎖的撤銷過程
偏向鎖的撤銷在上面第 4 點有說到, 偏向鎖使用了一種等到競爭出現才釋放偏向鎖的機制: 偏向鎖只有遇到其他線程嘗試競爭偏向鎖時, 持有偏向鎖的線程纔會釋放, 線程本身不會主動去釋放偏向鎖. 偏向鎖的撤銷需要等待全局安全點(在這個時間點上沒有字節碼正在執行), 它會首先暫停擁有偏向鎖的線程, 判斷鎖對象是否處於被鎖定狀態, 撤銷偏向鎖後恢復到無鎖或輕量級鎖的狀態. 我們發現, 這個開銷其實還是挺大的, 所以如果某些同步代碼塊大多數情況下都是有兩個及以上的線程競爭的話, 那麼偏向鎖就會值一種累贅, 對於這種情況, 建議一開始就把偏向鎖關閉.

注意: 偏向鎖撤銷是指在獲取偏向鎖的過程中因不滿足條件導致要將鎖對象改爲非偏向鎖狀態, 而偏向鎖釋放是指退出同步塊時的過程.

 
關閉偏向鎖
偏向鎖在 JDK 6JDK 7 中默認啓動的. 由於偏向鎖是爲了在只有一個線程執行同步塊的時候提高性能. 如果能確定應用程序裏所有的鎖通常情況下處於競爭狀態, 可以通過 JVM 參數關閉偏向鎖. 那麼程序默認會進入輕量級鎖的狀態.

  • 開啓偏向鎖: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 關閉偏向鎖:-XX:-UseBiasedLocking

 
偏向鎖流程圖

 

6. 鎖優化 - 輕量級鎖

輕量級鎖是由偏向鎖升級來的, 偏向鎖運行在一個線程進入同步塊的情況下, 當有第二個線程進入產生鎖競爭的情況下, 就會自動升級爲輕量級鎖. 其他線程會通過自旋的形式嘗試獲取鎖, 線程不會阻塞, 從而提高性能.

輕量級鎖的獲取主要有兩種情況

  • 當關閉偏向鎖功能時
  • 由於多個線程競爭偏向鎖導致偏向鎖升級爲輕量級鎖.
     

輕量級鎖獲取過程

  1. 拷貝對象頭中的 Mark Word 到當前線程棧幀的鎖記錄中. 並且虛擬機通過使用 CAS 操作嘗試將對象頭的 Mark Word 更新爲指向鎖記錄的指針, 並將鎖記錄裏的 owner 指針指向鎖對象. 這個操作成功執行步驟 2, 失敗執行步驟 3.

  2. 如果這個更新動作成功了, 那麼當前線程就擁有了該對象的鎖. 並且對象頭的 Mark Word 的鎖標誌位改爲 00, 即表示此對象處於輕量級鎖定狀態, 這時候線程堆棧與對象頭的狀態如下圖所示.

  1. 如果這個更新操作失敗了, 虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的棧幀
    • 如果不是: 說明這個鎖對象已經被其他線程搶佔了, 則通過自旋稍微等待一下, 有可能持有鎖的線程很快就會釋放鎖.
      但是當自旋超過一定次數(默認允許自旋 10 次, 可以通過虛擬機參數更改), 或者一個線程在持有鎖, 一個在自旋, 又有第三個線程來競爭的時候, 就會膨脹爲重量級鎖. 除了持有鎖的線程外, 其他線程阻塞. 對象頭Mark Word 中指向鎖記錄的指針改爲指向重量級鎖(互斥量)的指針, 同時將鎖標誌位改爲 10.
    • 如果是: 說明當前線程已經擁有了這個對象的鎖, 現在是重入狀態. 可直接進入同步塊繼續執行. 同時會添加一條鎖記錄 Lock Record, 其中 Displaced Mark Wordnull, 起到一個重入計數器的作用.

 

輕量級鎖解鎖過程

  1. 遍歷當前線程棧幀, 找到所有 owner 指向當前鎖對象的 Lock Record
  2. 如果 Lock RecordDisplaced Mark Wordnull 說明這是一次重入, 刪除此鎖記錄, 接着 continue . 這即爲一次解鎖結束.
  3. 如果 Displaced Mark Word 不爲 null, 並且對象頭中的 Mark Word 仍然指向當前線程的鎖記錄, 那就通過 CAS 操作把對象頭中的 Mark Word 恢復成爲 Lock Record 中拷貝過去的 Displaced Mark Word 值.
  4. 如果替換成功, 則 continue. 也即爲一次解鎖結束.
  5. 如果替換失敗. 說明外面有一個線程到達了自旋的總次數, 或者外面至少還有兩個線程來競爭鎖, 導致鎖已經膨脹爲重量級鎖. 從而改變了對象頭中 Mark Word 的內容. 那就要在釋放鎖的同時, 喚醒被掛起的線程. 重新爭奪鎖訪問同步塊.

輕量級鎖能提升程序同步性能的依據是 "對於絕大部分鎖在整個同步週期內都是不存在競爭的" 這是一個經驗數據. 如果沒有競爭, 輕量級鎖使用 CAS 操作避免了使用互斥量的開銷, 但是如果存在競爭, 除了互斥量的開銷外, 還額外發生了 CAS 操作. 因此在有競爭的情況下, 輕量級鎖會比傳統的重量級鎖更慢.

 

7. 鎖優化 - 重量級鎖

重量級鎖就是我們常說的傳統意義上的鎖, 其利用操作系統底層的同步機制去實現 Java 中的線程同步.
下圖是整個偏向鎖到輕量級鎖再膨脹爲重量級鎖的流程圖. 可能不是很清晰.

 

8. 鎖優化 - 鎖消除

何爲鎖消除?
鎖消除即刪除不必要的加鎖操作, 在介紹這個之前, 先說說 逃逸和逃逸分析.

逃逸是指在方法之內創建的對象, 除了在方法體之內被引用之外, 還在方法體之外被引用. 也就是說在方法體之外引用方法內的對象, 在方法執行完畢後, 方法中創建的對象應該被 GC 回收. 但是由於該對象被其他變量引用, 導致 GC 無法回收.

這個無法被回收的對象成爲 "逃逸" 對象. Java 中的逃逸分析, 就是對這種對象的分析.

那麼接着回到鎖消除, Java JIT Java 即時編譯 會通過逃逸分析的方式, 去分析加鎖的代碼是否被一個或者多個線程使用, 或者等待被使用. 如果分析證實, 只有一個線程訪問, 在編譯這個代碼段的時候, 就不會生成 Synchronized 關鍵字, 僅僅生代碼對應的機器碼.

換句話說, 即使我們開發人員加上了 Synchronized , 但是隻要 JIT 發現這段代碼只會被一個線程訪問, 也會把Synchronized 去掉.

 

9. 鎖優化 - 鎖粗化

如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有出現線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。

如果虛擬機檢測到有一串零碎的操作都是對同一對象的加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部.

    public static void main(String[] args) throws Exception {
        synchronized (object) {
            test();
        }
        // 中間可穿插其他代碼
        synchronized (object) {
            test1();
        }
        synchronized (object) {
            test2();
        }
    }

上面代碼存在三塊代碼段, 分割成三個臨界區, 在 JIT 編譯時會將其合併成一個臨界區. 用一個鎖對其進行訪問控制. 減少了鎖的獲取和釋放的次數. 編譯後的等效代碼如下

    public static void main(String[] args) throws Exception {
        synchronized (object) {
            test();
            test1();
            test2();
        }
    }

鎖粗化默認是開啓的。如果要關閉這個特性可以在 Java 程序的啓動命令行中添加虛擬機參數-XX:-EliminateLocks.

 

10. 鎖優化 - 自旋鎖與自適應自旋鎖

自旋鎖的來由
自旋鎖我們都知道是爲了讓該線程執行一段無意義的自旋, 等待一段時間, 不會被立刻掛起, 看持有鎖的線程是否會很快釋放.

可是爲什麼要引入自旋鎖呢?

首先互斥同步對性能最大的影響就是上面我們說過的阻塞的實現, 因爲阻塞和喚醒線程的操作都需要由用戶態轉到內核態中完成, 這些操作給系統的併發性能帶來很大壓力.

其次虛擬機的開發團隊也注意到許多應用上面, 共享數據的鎖定狀態只會持續很短一段時間, 爲了這一段很短的時間頻繁的阻塞喚醒線程非常不值得. 於是, 就引入了自旋鎖.

自旋鎖的缺點
自旋鎖雖然可以避免線程切換帶來的開銷, 但是它卻佔用了處理器的時間. 如果持有鎖的線程很快就釋放了鎖, 那麼自旋的效率就非常好. 反之, 自旋的線程就會白白浪費處理器的資源帶來性能上的浪費. 所以說自旋的次數必須要有一個限度, 例如 10 次. 如果超過這個次數還未獲取到鎖, 則就阻塞.

瞭解了自旋鎖, 那自適應的自旋鎖呢?

自適應自旋鎖
在 JDK1.6 中引入了自適應的自旋鎖, 自適應就意味着自旋的次數不再是固定的, 它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的.

如果在同一個鎖的對象上, 剛剛自旋成功獲得過鎖, 並且持有鎖的線程正在運行中, 那麼虛擬機就會認爲這次自旋也很有可能再次成功, 進而它將允許自旋等待持續更長時間.

如果對於某個說, 自旋很少成功過, 那麼在以後要獲取這個鎖時將可能省略掉自旋的過程, 以避免浪費處理器資源.

簡單來說, 就是線程如果自旋成功了, 則下次自旋的次數會更多, 如果自旋失敗了, 則自旋的次數減少.

 

OK, 到這裏也差不多了, 關於鎖的優化基本就分析完了, 最後來個總結吧.

11. 總結

偏向鎖 輕量級鎖 重量級鎖
本質 取消同步操作 CAS 操作代替互斥同步 互斥同步
優點 不阻塞, 執行效率高, 只有第一次獲取偏向鎖時需要 CAS 操作, 後面只需要對比線程 ID 不會阻塞 不會空耗 CPU
缺點 適用場景太侷限, 若產生競爭, 會有額外的偏向鎖撤銷的消耗 自旋會浪費 CPU 資源 阻塞喚醒, 用戶態切換到內核態. 重量級操作
  • synchronized 的特點: 保證了內存可見性, 操作的原子性.
  • synchronized 影響性能的原因
    • 加鎖和解鎖需要額外操作
    • 互斥同步對性能最大的影響就是阻塞的時間, 因爲阻塞喚醒會由用戶態轉到內核態中完成. 代價太大.

偏向鎖, 輕量級鎖, 重量級鎖都是 java 虛擬機自己內部實現, 當執行到 synchronized 同步代碼塊的時候, java 虛擬機會根據啓用的鎖和當前線程的爭用情況來決定如何執行同步操作.

在所有的鎖都啓用的情況下線程進入臨界區時會先獲得偏向鎖, 如果已經存在偏向鎖了, 則會嘗試獲取輕量級鎖, 啓用自旋鎖, 如果自旋也沒獲取到鎖, 則使用重量級鎖, 沒有獲取到鎖的線程被阻塞掛起, 知道持有鎖的線程執行完同步代碼塊後去喚醒它們.

如果線程爭用激烈, 那麼應該禁用偏向鎖.

不同的鎖有不同特點, 每種鎖只有在其特定的場景下, 纔會有出色的表現, java中沒有哪種鎖能夠在所有情況下都能有出色的效率. 引入這麼多鎖的原因就是爲了應對不同的情況.


網上也摘抄了不少博客上的內容, 自己整理了一下, 變成自己能看懂的.
參考來源:
Java synchronized原理總結
synchronized 底層原理
synchronized原理和鎖優化策略(偏向/輕量級/重量級)

至此本章到這裏就結束了, 看到這裏, 如果對你有幫助, 請點贊關注. 謝謝大家.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章