深入理解 synchronized
引言:synchronized的核心地位
在Java并發編程中,synchronized
關鍵字是實現線程安全的基石。自JDK 1.0引入以來,它經歷了從"重量級鎖"到"自適應鎖"的進化,如今已成為兼顧安全性與性能的成熟方案。本文將從用法解析、字節碼實現、底層原理、鎖升級機制、JDK優化、性能對比到最佳實踐,全方位剖析synchronized
的技術細節,結合OpenJDK源碼與實測數據,帶你徹底掌握這一并發利器。
一、synchronized的基本用法與語義
1.1 三種使用方式
synchronized
可修飾方法或代碼塊,核心是通過對象鎖實現線程互斥。具體用法如下:
用法場景 | 鎖對象 | 字節碼實現 | 示例代碼 |
---|---|---|---|
修飾實例方法 | 當前對象實例(this ) | 方法訪問標志ACC_SYNCHRONIZED | public synchronized void increment() { count++; } |
修飾靜態方法 | 類對象(Class 實例) | 方法訪問標志ACC_SYNCHRONIZED | public static synchronized void staticIncrement() { staticCount++; } |
修飾代碼塊 | 顯式指定對象 | monitorenter /monitorexit 指令 | synchronized (lockObj) { count++; } |
關鍵語義:
- 互斥性:同一時刻只有一個線程能持有鎖,確保臨界區代碼串行執行。
- 可見性:釋放鎖時,線程會將工作內存中的修改刷新到主內存;獲取鎖時,線程會失效本地緩存,從主內存加載最新值(通過內存屏障實現)。
- 可重入性:線程可重復獲取已持有的鎖,通過
_recursions
計數器實現(見ObjectMonitor源碼)。
1.2 用法示例與字節碼分析
示例1:同步代碼塊
public class SyncBlockExample {private final Object lock = new Object();private int count = 0;public void increment() {synchronized (lock) { // 顯式指定lock為鎖對象count++;}}
}
字節碼反編譯(javap -v SyncBlockExample.class
):
同步代碼塊通過monitorenter
(進入鎖)和monitorexit
(釋放鎖)指令實現:
public void increment();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: getfield #2 // Field lock:Ljava/lang/Object;4: dup5: astore_1 // 將鎖對象引用存入局部變量表6: monitorenter // 獲取鎖7: aload_08: dup9: getfield #3 // Field count:I12: iconst_113: iadd14: putfield #3 // Field count:I17: aload_118: monitorexit // 正常退出時釋放鎖19: goto 2722: astore_223: aload_124: monitorexit // 異常退出時釋放鎖25: aload_226: athrow27: return
注意:編譯器會生成兩個
monitorexit
,分別對應正常退出和異常退出,確保鎖必定釋放。
示例2:同步方法
public class SyncMethodExample {private int count = 0;public synchronized void increment() { // 實例方法鎖,鎖對象為thiscount++;}public static synchronized void staticIncrement() { // 靜態方法鎖,鎖對象為SyncMethodExample.classstaticCount++;}
}
字節碼特征:同步方法通過ACC_SYNCHRONIZED
標志實現,無需顯式monitor
指令:
public synchronized void increment();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZED // 同步方法標志Code:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field count:I5: iconst_16: iadd7: putfield #2 // Field count:I10: return
二、底層實現:對象頭與Monitor機制
2.1 對象頭與Mark Word
synchronized
的實現依賴對象頭(Object Header)中的Mark Word存儲鎖狀態。對象頭由兩部分組成:
- Mark Word:存儲對象運行時數據(哈希碼、GC年齡、鎖狀態等)。
- Klass Pointer:指向類元數據的指針。
64位JVM Mark Word格式(不同鎖狀態下的存儲結構):
鎖狀態 | 標志位 | 存儲內容 |
---|---|---|
無鎖 | 01 | 哈希碼(25bit)+ GC年齡(4bit)+ 是否偏向鎖(1bit=0)+ 標志位(2bit=01) |
偏向鎖 | 01 | 偏向線程ID(54bit)+ Epoch(2bit)+ GC年齡(4bit)+ 是否偏向鎖(1bit=1)+ 標志位(2bit=01) |
輕量級鎖 | 00 | 指向棧中鎖記錄(Lock Record)的指針(64bit)+ 標志位(2bit=00) |
重量級鎖 | 10 | 指向ObjectMonitor對象的指針(64bit)+ 標志位(2bit=10) |
GC標記 | 11 | 空 |
工具推薦:使用
org.openjdk.jol:jol-core
查看對象頭,如ClassLayout.parseInstance(obj).toPrintable()
。
2.2 Monitor監視器鎖
重量級鎖的實現依賴ObjectMonitor(C++實現),每個對象關聯一個Monitor,用于管理線程競爭與等待。
ObjectMonitor核心結構(OpenJDK源碼objectMonitor.hpp
):
class ObjectMonitor {
private:void* volatile _owner; // 持有鎖的線程ObjectWaiter* volatile _WaitSet; // 等待隊列(調用wait()的線程)ObjectWaiter* volatile _EntryList; // 阻塞隊列(未獲取鎖的線程)int _recursions; // 重入次數int _count; // 等待線程數// ...其他字段
};
工作流程:
- 競爭鎖:線程通過
CAS
嘗試將_owner
設為自身,成功則獲取鎖;失敗則進入_EntryList
阻塞。 - 釋放鎖:線程退出同步塊時,將
_owner
設為null
,喚醒_EntryList
中的線程重新競爭。 - 等待/喚醒:調用
wait()
時,線程釋放鎖并進入_WaitSet
;notify()
將線程從_WaitSet
移至_EntryList
重新競爭。
三、鎖升級機制:從偏向鎖到重量級鎖
JDK 1.6引入鎖升級機制,根據競爭程度動態選擇鎖狀態(不可逆),平衡性能與安全性。
3.1 偏向鎖(Biased Locking)
設計目標:減少單線程重復獲取鎖的開銷。
實現原理:
- 首次獲取鎖時,通過
CAS
將線程ID記錄到Mark Word,設為偏向模式(標志位101
)。 - 后續同一線程訪問時,僅需比對線程ID,無需CAS操作。
撤銷條件:當其他線程嘗試競爭時,需等待全局安全點(STW),暫停持有線程,檢查狀態:
- 若持有線程已結束,重置為無鎖狀態。
- 若持有線程存活,升級為輕量級鎖。
JVM參數:
-XX:+UseBiasedLocking
(默認啟用,JDK 15后默認禁用)。-XX:BiasedLockingStartupDelay=0
(禁用啟動延遲)。
3.2 輕量級鎖(Lightweight Locking)
設計目標:應對多線程交替執行的輕度競爭。
實現步驟:
- 創建鎖記錄:線程在棧幀中創建
Lock Record
,復制Mark Word(Displaced Mark Word)。 - CAS競爭鎖:通過
CAS
將Mark Word替換為指向Lock Record的指針(標志位00
)。 - 自旋重試:競爭失敗時,線程自旋(空循環)嘗試獲取鎖,避免阻塞(自適應自旋:根據歷史成功率調整次數)。
升級條件:
- 自旋超過閾值(默認10次,JDK 1.7后自適應)。
- 競爭線程數超過CPU核心數一半。
3.3 重量級鎖(Heavyweight Locking)
設計目標:應對高并發激烈競爭。
實現原理:
- Mark Word指向ObjectMonitor,未獲取鎖的線程進入
_EntryList
阻塞(操作系統級別的互斥鎖)。 - 線程阻塞/喚醒涉及用戶態→內核態切換,開銷較大。
性能對比:
鎖狀態 | 獲取成本 | 釋放成本 | 適用場景 |
---|---|---|---|
偏向鎖 | 極低 | 極低 | 單線程重復訪問 |
輕量級鎖 | 低(CAS) | 低(CAS) | 多線程交替執行 |
重量級鎖 | 高 | 高 | 多線程同時競爭 |
四、JDK優化:從鎖消除到虛擬線程
4.1 鎖優化技術
鎖消除(Lock Elimination)
JIT編譯器通過逃逸分析,消除不可能存在競爭的鎖。例如:
public String concat(String a, String b) {StringBuffer sb = new StringBuffer(); // StringBuffer的append是同步方法sb.append(a).append(b);return sb.toString();
}
// 逃逸分析發現sb未逃逸,消除同步鎖
鎖粗化(Lock Coarsening)
合并連續的鎖申請,減少鎖競爭頻率:
for (int i = 0; i < 1000; i++) {synchronized (lock) { // 循環內頻繁加鎖,粗化為一次鎖申請count++;}
}
// 優化后:synchronized (lock) { for (...) { count++; } }
自適應自旋(Adaptive Spinning)
JVM根據歷史自旋成功率動態調整次數:
- 若自旋成功,下次增加自旋次數(最大100次)。
- 若自旋失敗,減少或省略自旋,避免CPU空轉。
4.2 JDK 17的虛擬線程支持
JDK 17通過JEP 491優化synchronized
與虛擬線程(Virtual Threads)的兼容性,避免線程固定(Pinning):
- 虛擬線程阻塞于
synchronized
時,JVM自動卸載載體線程(Carrier Thread),允許其他虛擬線程復用。 - 實現原理:結合
Continuation
機制,在阻塞時保存棧幀,釋放載體線程。
性能提升:在高并發I/O場景,吞吐量提升30%+,避免傳統線程阻塞導致的資源浪費。
五、源碼深度剖析:ObjectMonitor關鍵方法
5.1 加鎖(enter方法)
void ATTR ObjectMonitor::enter(TRAPS) {Thread* Self = THREAD;void* cur = Atomic::cmpxchg_ptr(Self, &_owner, NULL); // CAS嘗試獲取鎖if (cur == NULL) { // 成功獲取鎖,_owner = Selfreturn;}if (cur == Self) { // 重入,_recursions++_recursions++;return;}// 競爭失敗,進入自旋或阻塞if (Knob_SpinEarly && TrySpin(Self) > 0) { // 自旋成功,獲取鎖return;}// 自旋失敗,進入_EntryList阻塞ThreadBlockInVM tbivm(Self);Self->set_current_pending_monitor(this);EnterI(Self); // 進入阻塞隊列
}
5.2 釋放鎖(exit方法)
void ATTR ObjectMonitor::exit(TRAPS) {if (_recursions != 0) { // 重入解鎖,_recursions--_recursions--;return;}// 喚醒_EntryList中的線程ObjectWaiter* w = NULL;w = _EntryList;if (w != NULL) {Atomic::cmpxchg_ptr(NULL, &_owner, Self); // 釋放鎖OrderAccess::fence();WakeupWaiter(w); // 喚醒線程return;}// 無等待線程,直接釋放Atomic::cmpxchg_ptr(NULL, &_owner, Self);
}
六、性能對比與最佳實踐
6.1 synchronized vs Lock(ReentrantLock)
特性 | synchronized | ReentrantLock |
---|---|---|
實現層級 | JVM層面(關鍵字) | JDK層面(接口) |
鎖釋放 | 自動釋放(異常/正常退出) | 手動釋放(需finally塊) |
高級功能 | 無(不可中斷、非公平) | 支持中斷、超時、公平鎖、條件變量 |
性能(低競爭) | 接近(偏向鎖/輕量級鎖) | 略高(CAS操作) |
性能(高競爭) | 重量級鎖開銷大 | 略優(隊列優化) |
建議:簡單同步需求用synchronized
(簡潔安全);需高級功能(如超時獲取)用ReentrantLock
。
6.2 生產環境最佳實踐
-
減小鎖粒度:同步代碼塊僅包裹臨界區,避免鎖范圍過大:
// 反例:整個方法加鎖 public synchronized void process() {readConfig(); // 無需同步的操作updateState(); // 需同步的臨界區 } // 正例:僅臨界區加鎖 public void process() {readConfig();synchronized (lock) {updateState();} }
-
避免嵌套鎖:減少死鎖風險,如必須嵌套,確保鎖順序一致。
-
禁用偏向鎖:高并發場景下,偏向鎖撤銷開銷大,通過
-XX:-UseBiasedLocking
禁用。 -
監控鎖狀態:使用
jstack
查看線程阻塞狀態,定位競爭熱點:jstack <pid> | grep -A 20 "BLOCKED" # 查看阻塞線程
-
JVM參數調優:
-XX:PreBlockSpin=10
:輕量級鎖自旋次數。-XX:BiasedLockingBulkRebiasThreshold=20
:批量重偏向閾值。-XX:BiasedLockingBulkRevokeThreshold=40
:批量撤銷閾值。
七、總結
synchronized
從早期的重量級鎖進化為如今的自適應鎖機制,體現了JVM對性能的極致追求。其核心在于動態鎖升級(偏向鎖→輕量級鎖→重量級鎖),結合對象頭Mark Word與ObjectMonitor實現高效同步。在JDK 17中,通過對虛擬線程的支持,進一步提升了高并發場景下的可擴展性。
掌握synchronized
的底層原理,不僅能寫出更高效的并發代碼,更能深入理解JVM的優化機制。在實際開發中,需結合業務場景選擇合適的鎖策略,平衡安全性與性能,避免過度同步或鎖競爭導致的性能瓶頸。
參考資料:
- OpenJDK源碼(hotspot/src/share/vm/runtime/objectMonitor.hpp)
- 《深入理解Java虛擬機》(周志明)
- JDK官方文檔(JEP 142、JEP 491)