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,通過分段的機制來控制。
總結
- 檢測Mark Word裡面是不是當前執行緒ID,如果是,表示當前執行緒處於偏向鎖
- 如果不是,則使用CAS將當前執行緒ID替換到Mark Word,如果成功則表示當前執行緒獲得偏向鎖,設定偏向標誌位1
- 如果失敗,則說明發生了競爭,撤銷偏向鎖,升級為輕量級鎖
- 當前執行緒使用CAS將物件頭的mark Word鎖標記位替換為鎖記錄指標,如果成功,當前執行緒獲得鎖
- 如果失敗,表示其他執行緒競爭鎖,當前執行緒嘗試通過自旋獲取鎖 for(;;)
- 如果自旋成功則依然處於輕量級狀態
- 如果自旋失敗,升級為重量級鎖