首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JDK 5.0 並發新選擇:ReentrantLock 靈活鎖定機制深度解析

     編輯:Java 相關知識

JDK 5.0 為開發高性能並發應用程式帶來了強有力的新工具。java.util.concurrent.locks 套件中的 ReentrantLock 類別,作為 Java 內建 synchronized 關鍵字的替代方案,不僅擁有相同的記憶體語義與鎖定行為,更在爭用情況下展現優異效能,並提供 synchronized 缺乏的進階功能。這是否意味著我們應拋棄 synchronized 完全轉向 ReentrantLock?並發專家 Brian Goetz 將為我們揭曉答案。

synchronized 快速回顧

將程式碼區塊宣告為 synchronized 具有兩大核心語義:原子性 與 可見性。原子性確保同一時間僅有一個執行緒能執行受特定監視器(鎖)保護的程式碼,避免多執行緒更新共享狀態時發生衝突。可見性則處理記憶體快取與編譯器優化帶來的複雜行為:若未同步,執行緒對變數的修改不一定立即被其他執行緒看見;但若使用同步,執行緒在退出 synchronized 區塊前對變數的更新,將在另一執行緒進入由同一鎖保護的 synchronized 區塊時立即可見。volatile 變數也遵循類似規則。

synchronized (lockObject) {
 // 更新物件狀態
}

正確同步能確保共享變數安全更新、無競態條件、資料不受破壞,且其他正確同步的執行緒能看見最新值。JDK 5.0 修正了原有 Java 記憶體模型的缺陷,遵循一條簡單原則即可構建「一次編寫,到處執行」的並發類別:

無論何時,只要您寫入的變數稍後可能被另一執行緒讀取,或您讀取的變數剛被另一執行緒寫入,您就必須同步。

現代 JVM 中,無爭用同步(鎖未被其他執行緒爭搶)的效能開銷極低。早期 JVM 同步未優化導致「同步昂貴」的誤解流傳至今,實則不然。

synchronized 的侷限性

雖然 synchronized 已相當優秀,但並非完美。功能性限制包括:

  • 無法中斷正在等待獲取鎖的執行緒
  • 無法嘗試非阻塞式獲取鎖(投票機制)
  • 鎖的釋放必須發生在獲取鎖的相同堆疊幀中(塊結構鎖定),雖多數情況無礙且與異常處理配合良好,但確有非塊結構鎖定更適合的場景

ReentrantLock 類別詳解

java.util.concurrent.locks.Lock 介面將鎖定實現抽象為 Java 類別而非語言特性,為多種實現留出空間(不同調度演算法、效能特徵、鎖定語義)。ReentrantLock 實現了 Lock,具備與 synchronized 相同的並發與記憶體語義,並新增:

  • 鎖投票(tryLock())
  • 定時鎖等待(tryLock(timeout))
  • 可中斷鎖等待(lockInterruptibly())
  • 激烈爭用下更優的可伸縮性能

「可重入」意指鎖擁有獲取計數器:持有鎖的執行緒重複獲取時計數器加一,需釋放同樣次數才真正釋放鎖,模仿 synchronized 語義。

關鍵差異:必須在 finally 區塊釋放鎖

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 明顯領先。

條件變數:Condition 介面

Object 的 wait()、notify()、notifyAll() 是高階並發特性,使用微妙且易誤用。Lock 框架將其泛化為 Condition(條件):

  • Lock 物件作為綁定條件變數的工廠
  • 單一 Lock 可關聯多個條件變數(如「非滿」、「非空」),簡化並發演算法開發
  • 方法對應:await、signal、signalAll(避免覆寫 Object 方法)

公平鎖 vs 非公平鎖

ReentrantLock 建構子接受 boolean 參數選擇公平或非公平模式:

  • 公平鎖:執行緒按請求順序獲取鎖
  • 非公平鎖:允許「插隊」,後請求者可能先獲得鎖

公平性保證帶來巨大效能代價(記帳與同步開銷),導致爭用公平鎖吞吐量顯著低於非公平鎖。預設為非公平(false),除非演算法嚴格要求先來先服務。

令人驚訝的是:內建監視器鎖(synchronized)也是非公平的,且從未引發執行緒飢餓問題,因 JVM 保證統計公平性(所有執行緒最終都能獲取鎖),成本遠低於絕對公平。

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

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

是否應拋棄 synchronized?

ReentrantLock 似乎全方位優於 synchronized:功能超集、相同語義、爭用下效能更佳。部分入門書籍甚至完全以 Lock 為例,將 synchronized 視為歷史遺物。但 Brian Goetz 認為這是「好事做過頭」。

保留 synchronized 的理由

  1. 自動釋放鎖,不易出錯:synchronized 區塊退出時 JVM 自動釋放鎖;ReentrantLock 若忘記在 finally 中釋放,程式測試通過但在生產環境死鎖,極難排查。這也是不建議初級開發者使用 Lock 的原因。
  2. 除錯支援:JVM 管理 synchronized 鎖請求與釋放,執行緒傾印可包含鎖資訊,識別死鎖來源極具價值。Lock 只是普通類別,JVM 無法知悉哪個執行緒持有鎖。
  3. 普及度與相容性:幾乎所有開發者熟悉 synchronized,且在所有 JVM 版本運作。JDK 5.0 普及前(可能需兩年),使用 Lock 意味著依賴非所有 JVM 具備且非所有開發者熟悉的特性。

何時選擇 ReentrantLock

答案簡單:確實需要 synchronized 缺乏的特性時,如:

  • 定時鎖等待
  • 可中斷鎖等待
  • 非塊結構鎖定
  • 多條件變數
  • 鎖投票

雖具可伸縮性優勢,僅在高度爭用場景建議使用。但大多數 synchronized 區塊幾乎無爭用,故可暫不考慮。建議:優先使用 synchronized 開發,直到證明其不適用,而非假設 ReentrantLock「效能會更好」。這些是高階工具,真正的高手傾向選擇最簡單工具,直到證明不適用。永遠先把事情做對,再考慮做快。

結語

Lock 框架是 synchronized 的相容替代品,提供更多特性與爭用下更佳效能。但這些優勢不足以成為全面替代的理由。選擇應基於:您是否真正需要 ReentrantLock 的能力。絕大多數情況下,synchronized 運作良好、全 JVM 相容、開發者熟悉、不易出錯。唯有真正需要 Lock 時才使用它——那時您會慶幸擁有此利器。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved