在 Java 多線程程式設計中,synchronized 是實現線程同步最基礎、最常用的關鍵字。它就像一把「互斥鎖」,確保同一時間只有一個線程能夠訪問受保護的代碼區域,從而避免數據競爭與不一致問題。本文將從底層原理、使用場景、字節碼層面以及常見誤區等多個維度,全面剖析 synchronized 的核心機制。
Java 中的每一個對象在內存中都關聯著一個 Monitor(監視器/管程),這就是所謂的「對象鎖」。當多個線程嘗試同時訪問同一個對象的同步代碼時,JVM 會要求線程先獲取該對象的 Monitor。只有成功獲取鎖的線程才能進入同步區執行;其他線程則會進入阻塞隊列等待,直到持有鎖的線程釋放鎖(正常退出或拋出異常)為止。
public synchronized void doSomething() {
// 臨界區代碼
}
鎖對象是 this(當前實例對象)。
public void doSomething() {
synchronized(this) { // 或 synchronized(lockObject)
// 臨界區代碼
}
}
鎖對象由開發者指定,靈活性更高,粒度更細,能有效減少同步範圍,提升並發性能。
public static synchronized void staticMethod() { ... }
鎖對象是當前類的 Class 對象(即 CurrentClass.class),屬於類級別的鎖,與實例鎖互不干擾。
obj 的同步方法時,線程 B 無法同時執行 obj 的任何其他同步方法(無論方法名是否相同),因為它們共享同一把鎖(this 或指定對象)。obj1 的同步方法,線程 B 訪問 obj2 的同步方法(即使是同一類的實例),兩者並行無阻塞,因為鎖屬於不同對象實例。通過 javap -c 反編譯字節碼可見,synchronized 的實現依賴兩條指令:
monitorenter:進入同步塊前嘗試獲取鎖(計數器 +1)。monitorexit:退出同步塊時釋放鎖(計數器 -1),編譯器會在正常退出路徑和異常處理路徑分別插入此指令,保證鎖必定被釋放。這也是為什麼 synchronized 不會因異常導致死鎖的原因——JVM 幫我們兜底了。
若方法中只有小部分涉及共享資源,優先使用 同步代碼塊 縮小鎖範圍,避免不必要的阻塞,提高吞吐量。
class Demo {
public synchronized void instanceMethod() { ... } // 鎖 this
public static synchronized void staticMethod() { ... } // 鎖 Demo.class
}
一個線程執行 instanceMethod,另一個線程執行 staticMethod,不會互斥,因為鎖對象不同。
public void methodBBB() {
synchronized(Demo.class) { // 等價於 static synchronized
// ...
}
}
雖然 synchronized 經過 JDK 1.6+ 的大量優化(偏向鎖、輕量級鎖、鎖消除、鎖粗化、自旋鎖等),性能已大幅提升,但仍存在固有缺陷:
interrupt(),只能被動等待。wait/notify 隊列,無法實現多條件變量精細控制。為解決上述問題,JDK 1.5 引入了 java.util.concurrent.locks.Lock 接口及其實現類 ReentrantLock,提供了可中斷、可超時、公平/非公平可選、多條件變量等豐富功能,成為高並發場景下的首選。
this。String、包裝類)不適合作為鎖對象,因常量池緩存可能導致非預期共享鎖。ReentrantLock 或 ReadWriteLock。ConcurrentHashMap、CopyOnWriteArrayList 等)與原子類(AtomicInteger 等),減少手動同步需求。掌握 synchronized 的本質與邊界,是通往 Java 並發編程高手之路的必修課。理解其底層 Monitor 機制、字節碼語義及 JVM 優化手段,才能在實戰中做出正確的架構決策。