1. 程式人生 > 程式設計 >Synchronized同步鎖實現原理

Synchronized同步鎖實現原理

修飾程式碼塊

    // 關鍵字在程式碼塊上,鎖為括號裡面的物件
    public void method2() {
        Object o = new Object();
        synchronized (o) {
            // code
        }
    }
複製程式碼

Synchronized 在修飾同步程式碼塊時,是由 monitorenter 和 monitorexit 指令來實現同步的。進入 monitorenter 指令後,執行緒將持有 Monitor 物件,退出 monitorenter 指令後,執行緒將釋放該 Monitor 物件。

  // access flags 0x1
  public method2()V
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 16 L4
    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 1
   L5
    LINENUMBER 17 L5
    ALOAD 1
    DUP
    ASTORE 2
    MONITORENTER
   L0
    LINENUMBER 19 L0
    ALOAD 2
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [com/dragon/learn/leean1/SynchronizedTest java/lang/Object java/lang/Object] [java/lang/Throwable]
    ASTORE 3
    ALOAD 2
    MONITOREXIT
   L3
    ALOAD 3
    ATHROW
   L6
    LINENUMBER 20 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE this Lcom/dragon/learn/leean1/SynchronizedTest; L4 L7 0
    LOCALVARIABLE o Ljava/lang/Object; L5 L7 1
    MAXSTACK = 2
    MAXLOCALS = 4
}
複製程式碼

修飾方法

當 Synchronized 修飾同步方法時,並沒有發現 monitorenter 和 monitorexit 指令,而是出現了一個 ACC_SYNCHRONIZED 標誌。

Monitor

JVM 中的同步是基於進入和退出管程(Monitor)物件實現的。每個物件例項都會有一個 Monitor,Monitor 可以和物件一起建立、銷燬。Monitor 是由 ObjectMonitor 實現,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 檔案實現,如下所示:

ObjectMonitor() {
   _header = NULL;
   _count = 0; // 記錄個數
   _waiters = 0,_recursions = 0;
   _object = NULL;
   _owner = NULL;
   _WaitSet = NULL; // 處於 wait
狀態的執行緒,會被加入到 _WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 處於等待鎖 block 狀態的執行緒,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } 複製程式碼

當多個執行緒同時訪問同一個程式碼塊時,首先會將這先執行緒放入ContenionList和EntryList中。之後執行緒通過作業系統的Mutex Lock來獲取鎖。如果獲取到了,則執行相應的程式碼。如果沒有獲取到,則重新進入ContenionList。如果呼叫了wait方法,則會進入WaitSet。當其他執行緒呼叫notify方法時會喚醒並重新進入EntryList.

鎖升級優化

Java物件頭

Java物件有物件頭,例項資料,填充資料三部分組成。其中物件頭由標記欄位,型別指標,陣列長度三部分組成。

偏向鎖

偏向鎖主要是用來優化同一個執行緒多次申請同一個鎖的競爭。偏向鎖的作用時當一個執行緒再次訪問同步程式碼或方法時,只需在物件頭上判斷執行緒的偏向鎖的執行緒ID是否為當前執行緒。如果是的話,則不用再次進入Monitor去競爭物件了。

如果有其他執行緒競爭該資源時,則改偏向鎖就會被撤消。偏向鎖的撤銷需要等待全域性安全點,暫停持有該鎖的執行緒。同時檢查該執行緒是否還在執行該方法,如果是,則升級鎖,反之,則其他執行緒搶佔。

因此,在高併發場景下,當大量執行緒同時競爭同一個鎖資源時,偏向鎖就會被撤銷,發生 stop the word 後, 開啟偏向鎖無疑會帶來更大的效能開銷,這時我們可以通過新增 JVM 引數關閉偏向鎖來調優系統效能,示例程式碼如下:

偏向鎖設定方法


-XX:-UseBiasedLocking //關閉偏向鎖(預設開啟)

-XX:+UseHeavyMonitors  //設定重量級鎖
複製程式碼

輕量級鎖

當另外有一個執行緒獲取鎖時,發現該鎖已經是偏向鎖了,那麼就會通過CAS的方式去獲取鎖,如果獲取成功,那麼直接替換標記欄位的型別執行緒ID為當前執行緒。如果獲取失敗,那麼就會撤偏向鎖,轉為輕量級鎖。

輕量級鎖適用於執行緒交替執行同步塊的場景,絕大部分的鎖在整個同步週期內都不存在長時間的競爭。

自旋鎖和重量級鎖

輕量級鎖CAS獲取鎖失敗, 預設會通過自旋的方式來獲取鎖。自旋鎖重試之後如果搶鎖依然失敗,同步鎖就會升級至重量級鎖,鎖標誌位改為 10。在這個狀態下,未搶到鎖的執行緒都會進入 Monitor,之後會被阻塞在 _WaitSet 佇列中。

鎖消除與鎖粗化

JIT編譯器在動態編譯同步程式碼塊的時候,會通過逃逸分析的技術。如果確定這個程式碼塊只會被一個執行緒訪問,那麼就會進行鎖消除。

鎖粗化同理,就是在 JIT 編譯器動態編譯時,如果發現幾個相鄰的同步塊使用的是同一個鎖例項,那麼 JIT 編譯器將會把這幾個同步塊合併為一個大的同步塊,從而避免一個執行緒“反覆申請、釋放同一個鎖“所帶來的效能開銷。

減小鎖粒度

這個主要是在程式碼層面進行優化。

例如,JDK8之前的ConcurrentHashMap,通過分段的機制來控制。

總結

  1. 檢測Mark Word裡面是不是當前執行緒ID,如果是,表示當前執行緒處於偏向鎖
  2. 如果不是,則使用CAS將當前執行緒ID替換到Mark Word,如果成功則表示當前執行緒獲得偏向鎖,設定偏向標誌位1
  3. 如果失敗,則說明發生了競爭,撤銷偏向鎖,升級為輕量級鎖
  4. 當前執行緒使用CAS將物件頭的mark Word鎖標記位替換為鎖記錄指標,如果成功,當前執行緒獲得鎖
  5. 如果失敗,表示其他執行緒競爭鎖,當前執行緒嘗試通過自旋獲取鎖 for(;;)
  6. 如果自旋成功則依然處於輕量級狀態
  7. 如果自旋失敗,升級為重量級鎖