Synchronize 關鍵字原理

crossoverJie發表於2019-03-04

Synchronize 關鍵字原理

眾所周知 Synchronize 關鍵字是解決併發問題常用解決方案,有以下三種使用方式:

  • 同步普通方法,鎖的是當前物件。
  • 同步靜態方法,鎖的是當前 Class 物件。
  • 同步塊,鎖的是 {} 中的物件。

實現原理: JVM 是通過進入、退出物件監視器( Monitor )來實現對方法、同步塊的同步的。

具體實現是在編譯之後在同步方法呼叫前加入一個 monitor.enter 指令,在退出方法和異常處插入 monitor.exit 的指令。

其本質就是對一個物件監視器( Monitor )進行獲取,而這個獲取過程具有排他性從而達到了同一時刻只能一個執行緒訪問的目的。

而對於沒有獲取到鎖的執行緒將會阻塞到方法入口處,直到獲取鎖的執行緒 monitor.exit 之後才能嘗試繼續獲取鎖。

流程圖如下:

Synchronize 關鍵字原理

通過一段程式碼來演示:

    public static void main(String[] args) {
        synchronized (Synchronize.class){
            System.out.println("Synchronize");
        }
    }
複製程式碼

使用 javap -c Synchronize 可以檢視編譯之後的具體資訊。

public class com.crossoverjie.synchronize.Synchronize {
  public com.crossoverjie.synchronize.Synchronize();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/crossoverjie/synchronize/Synchronize
       2: dup
       3: astore_1
       **4: monitorenter**
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Synchronize
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      **14: monitorexit**
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}
複製程式碼

可以看到在同步塊的入口和出口分別有 monitorenter,monitorexit 指令。

鎖優化

synchronize 很多都稱之為重量鎖,JDK1.6 中對 synchronize 進行了各種優化,為了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖輕量鎖

輕量鎖

當程式碼進入同步塊時,如果同步物件為無鎖狀態時,當前執行緒會在棧幀中建立一個鎖記錄(Lock Record)區域,同時將鎖物件的物件頭中 Mark Word 拷貝到鎖記錄中,再嘗試使用 CASMark Word 更新為指向鎖記錄的指標。

如果更新成功,當前執行緒就獲得了鎖。

如果更新失敗 JVM 會先檢查鎖物件的 Mark Word 是否指向當前執行緒的鎖記錄。

如果是則說明當前執行緒擁有鎖物件的鎖,可以直接進入同步塊。

不是則說明有其他執行緒搶佔了鎖,如果存在多個執行緒同時競爭一把鎖,輕量鎖就會膨脹為重量鎖

解鎖

輕量鎖的解鎖過程也是利用 CAS 來實現的,會嘗試鎖記錄替換回鎖物件的 Mark Word 。如果替換成功則說明整個同步操作完成,失敗則說明有其他執行緒嘗試獲取鎖,這時就會喚醒被掛起的執行緒(此時已經膨脹為重量鎖)

輕量鎖能提升效能的原因是:認為大多數鎖在整個同步週期都不存在競爭,所以使用 CAS 比使用互斥開銷更少。但如果鎖競爭激烈,輕量鎖就不但有互斥的開銷,還有 CAS 的開銷,甚至比重量鎖更慢。

偏向鎖

為了進一步的降低獲取鎖的代價,JDK1.6 之後還引入了偏向鎖。

偏向鎖的特徵是:鎖不存在多執行緒競爭,並且應由一個執行緒多次獲得鎖。

當執行緒訪問同步塊時,會使用 CAS 將執行緒 ID 更新到鎖物件的 Mark Word 中,如果更新成功則獲得偏向鎖,並且之後每次進入這個物件鎖相關的同步塊時都不需要再次獲取鎖了。

釋放鎖

當有另外一個執行緒獲取這個鎖時,持有偏向鎖的執行緒就會釋放鎖,釋放時會等待全域性安全點(這一時刻沒有位元組碼執行),接著會暫停擁有偏向鎖的執行緒,根據鎖物件目前是否被鎖來判定將物件頭中的 Mark Word 設定為無鎖或者是輕量鎖狀態。

輕量鎖可以提高帶有同步卻沒有競爭的程式效能,但如果程式中大多數鎖都存在競爭時,那偏向鎖就起不到太大作用。可以使用 -XX:-userBiasedLocking=false 來關閉偏向鎖,並預設進入輕量鎖。

其他優化

適應性自旋

在使用 CAS 時,如果操作失敗,CAS 會自旋再次嘗試。由於自旋是需要消耗 CPU 資源的,所以如果長期自旋就白白浪費了 CPUJDK1.6加入了適應性自旋:

如果某個鎖自旋很少成功獲得,那麼下一次就會減少自旋。

號外

最近在總結一些 Java 相關的知識點,感興趣的朋友可以一起維護。

地址: github.com/crossoverJi…

Synchronize 關鍵字原理