JDK 5.0 為開發高性能並發應用程式帶來了強有力的新工具。java.util.concurrent.locks 套件中的 ReentrantLock 類別,作為 Java 內建 synchronized 關鍵字的替代方案,不僅擁有相同的記憶體語義與鎖定行為,更在爭用情況下展現優異效能,並提供 synchronized 缺乏的進階功能。這是否意味著我們應拋棄 synchronized 完全轉向 ReentrantLock?並發專家 Brian Goetz 將為我們揭曉答案。
將程式碼區塊宣告為 synchronized 具有兩大核心語義:原子性 與 可見性。原子性確保同一時間僅有一個執行緒能執行受特定監視器(鎖)保護的程式碼,避免多執行緒更新共享狀態時發生衝突。可見性則處理記憶體快取與編譯器優化帶來的複雜行為:若未同步,執行緒對變數的修改不一定立即被其他執行緒看見;但若使用同步,執行緒在退出 synchronized 區塊前對變數的更新,將在另一執行緒進入由同一鎖保護的 synchronized 區塊時立即可見。volatile 變數也遵循類似規則。
synchronized (lockObject) {
// 更新物件狀態
}
正確同步能確保共享變數安全更新、無競態條件、資料不受破壞,且其他正確同步的執行緒能看見最新值。JDK 5.0 修正了原有 Java 記憶體模型的缺陷,遵循一條簡單原則即可構建「一次編寫,到處執行」的並發類別:
無論何時,只要您寫入的變數稍後可能被另一執行緒讀取,或您讀取的變數剛被另一執行緒寫入,您就必須同步。
現代 JVM 中,無爭用同步(鎖未被其他執行緒爭搶)的效能開銷極低。早期 JVM 同步未優化導致「同步昂貴」的誤解流傳至今,實則不然。
雖然 synchronized 已相當優秀,但並非完美。功能性限制包括:
java.util.concurrent.locks.Lock 介面將鎖定實現抽象為 Java 類別而非語言特性,為多種實現留出空間(不同調度演算法、效能特徵、鎖定語義)。ReentrantLock 實現了 Lock,具備與 synchronized 相同的並發與記憶體語義,並新增:
tryLock())tryLock(timeout))lockInterruptibly())「可重入」意指鎖擁有獲取計數器:持有鎖的執行緒重複獲取時計數器加一,需釋放同樣次數才真正釋放鎖,模仿 synchronized 語義。
Lock lock = new ReentrantLock();
lock.lock();
try {
// 更新物件狀態
} finally {
lock.unlock();
}
若受保護程式碼拋出異常且未在 finally 中釋放,鎖將永不釋放,埋下難以排查的死鎖隱患。相比之下,synchronized 由 JVM 自動確保鎖釋放。
Tim Peierls 以線性同餘偽隨機數生成器(PRNG)為基準測試。每次 nextRandom() 呼叫皆執行實際運算,而非空跑。測試對比 synchronized 與 ReentrantLock 兩種實現,啟動大量執行緒瘋狂爭搶時間片,統計每秒吞吐量。
圖 1. 單 CPU 環境下吞吐量對比

圖 2. 四 CPU 環境下標準化吞吐量對比

圖表顯示:synchronized 版本在任何爭用下表現皆差,而 Lock 版本調度開銷極小,實現更高 CPU 利用率與吞吐量。未來 JVM 可能優化 synchronized 爭用效能,但目前 ReentrantLock 明顯領先。
Object 的 wait()、notify()、notifyAll() 是高階並發特性,使用微妙且易誤用。Lock 框架將其泛化為 Condition(條件):
Lock 物件作為綁定條件變數的工廠Lock 可關聯多個條件變數(如「非滿」、「非空」),簡化並發演算法開發await、signal、signalAll(避免覆寫 Object 方法)ReentrantLock 建構子接受 boolean 參數選擇公平或非公平模式:
公平性保證帶來巨大效能代價(記帳與同步開銷),導致爭用公平鎖吞吐量顯著低於非公平鎖。預設為非公平(false),除非演算法嚴格要求先來先服務。
令人驚訝的是:內建監視器鎖(synchronized)也是非公平的,且從未引發執行緒飢餓問題,因 JVM 保證統計公平性(所有執行緒最終都能獲取鎖),成本遠低於絕對公平。
圖 3. 四 CPU:同步、非公平鎖、公平鎖相對吞吐量

圖 4. 單 CPU:同步、非公平鎖、公平鎖相對吞吐量

ReentrantLock 似乎全方位優於 synchronized:功能超集、相同語義、爭用下效能更佳。部分入門書籍甚至完全以 Lock 為例,將 synchronized 視為歷史遺物。但 Brian Goetz 認為這是「好事做過頭」。
synchronized 區塊退出時 JVM 自動釋放鎖;ReentrantLock 若忘記在 finally 中釋放,程式測試通過但在生產環境死鎖,極難排查。這也是不建議初級開發者使用 Lock 的原因。synchronized 鎖請求與釋放,執行緒傾印可包含鎖資訊,識別死鎖來源極具價值。Lock 只是普通類別,JVM 無法知悉哪個執行緒持有鎖。synchronized,且在所有 JVM 版本運作。JDK 5.0 普及前(可能需兩年),使用 Lock 意味著依賴非所有 JVM 具備且非所有開發者熟悉的特性。答案簡單:確實需要 synchronized 缺乏的特性時,如:
雖具可伸縮性優勢,僅在高度爭用場景建議使用。但大多數 synchronized 區塊幾乎無爭用,故可暫不考慮。建議:優先使用 synchronized 開發,直到證明其不適用,而非假設 ReentrantLock「效能會更好」。這些是高階工具,真正的高手傾向選擇最簡單工具,直到證明不適用。永遠先把事情做對,再考慮做快。
Lock 框架是 synchronized 的相容替代品,提供更多特性與爭用下更佳效能。但這些優勢不足以成為全面替代的理由。選擇應基於:您是否真正需要 ReentrantLock 的能力。絕大多數情況下,synchronized 運作良好、全 JVM 相容、開發者熟悉、不易出錯。唯有真正需要 Lock 時才使用它——那時您會慶幸擁有此利器。