承接上文
上篇解析了 Doug Lea 早期 concurrent 包中 WriterPreferenceReadWriteLock(寫優先鎖)的基礎實現。本文續析其可重入版本 ReentrantWriterPreferenceReadWriteLock 以及 讀優先鎖 ReaderPreferenceReadWriteLock 的核心差異與設計考量。
1. ReentrantWriterPreferenceReadWriteLock - 可重入寫優先鎖
核心設計目標
- 允許同一線程重複獲取讀鎖或寫鎖(鎖計數器機制)
- 保持寫優先語義:有寫線程等待時,新讀線程必須阻塞
- 支援「寫線程可降級為讀」、「讀線程可升級為寫」(需謹慎使用)
新增關鍵成員變數
/** 當前持有寫鎖的線程重入次數 */
protected long writeHolds_ = 0;
/** 每個讀線程的重入計數:Key=Thread, Value=Integer(計數) */
protected HashMap readers_ = new HashMap();
/** 常量池優化:避免頻繁建立 Integer(1) */
protected static final Integer IONE = new Integer(1);
讀鎖核心邏輯重寫
// 判斷是否允許讀線程獲取鎖
protected boolean allowReader() {
// 原邏輯:無活躍寫線程且無等待寫線程
// 新增:若當前線程已持有寫鎖,允許重入讀(寫降級讀)
return (activeWriter_ == null && waitingWriters_ == 0) ||
activeWriter_ == Thread.currentThread();
}
// 獲取讀鎖
protected synchronized boolean startRead() {
Thread t = Thread.currentThread();
Object c = readers_.get(t);
if (c != null) { // 已持有讀鎖,計數+1
readers_.put(t, new Integer(((Integer)c).intValue() + 1));
++activeReaders_;
return true;
}
if (allowReader()) { // 首次獲取
readers_.put(t, IONE);
++activeReaders_;
return true;
}
return false; // 需等待
}
// 釋放讀鎖
protected synchronized Signaller endRead() {
Thread t = Thread.currentThread();
Object c = readers_.get(t);
if (c == null) throw new IllegalStateException();
--activeReaders_;
if (c != IONE) { // 重入計數 > 1,僅遞減
int h = ((Integer)c).intValue() - 1;
readers_.put(t, (h == 1) ? IONE : new Integer(h));
return null; // 不喚醒其他線程
} else { // 計數歸零,移除記錄
readers_.remove(t);
if (writeHolds_ > 0)
return null; // 當前線程仍持有寫鎖,不喚醒寫線程
// 邏輯同父類:無讀者且有等待寫者 → 喚醒寫者
if (activeReaders_ == 0 && waitingWriters_ > 0)
return writerLock_;
return null;
}
}
寫鎖核心邏輯重寫
// 獲取寫鎖
protected synchronized boolean startWrite() {
// 情況 1:當前線程已持有寫鎖 → 重入
if (activeWriter_ == Thread.currentThread()) {
++writeHolds_;
return true;
}
// 情況 2:無寫鎖重入記錄
if (writeHolds_ == 0) {
// 允許條件:無活躍讀者,或唯一活躍讀者就是當前線程(讀升級寫)
if (activeReaders_ == 0 ||
(readers_.size() == 1 && readers_.get(Thread.currentThread()) != null)) {
activeWriter_ = Thread.currentThread();
writeHolds_ = 1;
return true;
}
return false;
}
return false; // 其他線程持有寫鎖
}
// 釋放寫鎖
protected synchronized Signaller endWrite() {
--writeHolds_;
if (writeHolds_ > 0)
return null; // 重入計數未歸零,繼續持有
// 計數歸零,真正釋放
activeWriter_ = null;
if (waitingReaders_ > 0 && allowReader())
return readerLock_; // 喚醒所有等待讀者
else if (waitingWriters_ > 0)
return writerLock_; // 喚醒一個等待寫者
return null;
}
關鍵設計細節解析
| 場景 | 處理邏輯 | 設計意圖 |
| 寫線程重入寫鎖 | writeHolds_++ 直接成功 | 避免死鎖,支援遞歸調用 |
| 寫線程重入讀鎖 | allowReader() 檢測 activeWriter_ == currentThread 允許 | 支援鎖降級:寫→讀,釋放寫鎖前可讀取共享狀態 |
| 讀線程嘗試升級寫鎖 | startWrite() 檢測 readers_.size()==1 && 當前線程在內 允許 | 支援鎖升級:讀→寫,極易導致死鎖(兩讀線程同時升級),生產慎用 |
| 釋放讀鎖但仍持有寫鎖 | endRead() 檢測 writeHolds_ > 0 不喚醒寫者 | 確保寫鎖完全釋放前,不讓其他寫者搶佔 |
2. ReaderPreferenceReadWriteLock - 讀優先鎖
核心差異:僅重寫 allowReader()
protected boolean allowReader() {
// 只要沒有線程持有寫鎖,即允許讀(忽略等待寫者)
return activeWriter_ == null;
}
語義對比
| 特性 | 寫優先 | 讀優先 |
| 新讀者到來時 | 若有等待寫者 → 阻塞 | 若無活躍寫者 → 直接獲取 |
| 寫線程饑餓風險 | 低(寫者優先獲取) | 高(持續讀流可無限阻塞寫者) |
| 讀吞吐量 | 較低(寫者等待期間新讀者阻塞) | 極高(讀者幾乎無阻塞) |
| 典型適用場景 | 寫操作延遲敏感、資料一致性要求高 | 讀多寫極少、寫操作可容忍長延遲 |
為何實務極少用讀優先鎖?
- 寫線程饑餓:高併發讀場景下,寫線程可能永遠無法獲取鎖
- 資料不一致窗口放大:長時間讀併發期間,寫入被延遲,舊資料被持續讀取
- JDK ReentrantReadWriteLock 預設即公平/非公平寫優先:業界標準實踐傾向寫優先或公平策略
3. 從早期實現到 JDK 標準庫的演進
| 特性 | Doug Lea 早期版 | JDK ReentrantReadWriteLock |
| 同步基礎 | 自定義 Sync 介面 + wait/notify | AbstractQueuedSynchronizer (AQS) 狀態機 |
| 鎖狀態編碼 | 分離變數 activeReaders_、writeHolds_ | 單一 int state:高 16 位讀計數,低 16 位寫計數 |
| 公平性 | 僅寫優先/讀優先兩種 | 支援公平/非公平兩種模式(構造函數指定) |
| Condition 支援 | 無 | 寫鎖支援 newCondition(),讀鎖不支援 |
| 鎖降級支援 | 支援(寫→讀) | 支援(需先獲寫鎖再獲讀鎖,最後釋放寫鎖) |
| 鎖升級支援 | 支援(讀→寫,風險自擔) | 不支援(嘗試升級會死鎖) |
總結
ReentrantWriterPreferenceReadWriteLock 通過線程級計數器實現重入語義,並在allowReader、startWrite中引入當前線程身份判斷,優雅支援了鎖降級與受控升級。而 ReaderPreferenceReadWriteLock 雖實現簡單,但因寫饑餓風險鮮用於生產。現代 Java 開發應直接使用 java.util.concurrent.locks.ReentrantReadWriteLock(基於 AQS,效能更優、功能更全),僅在極特殊場景(如需自定義公平策略、鎖升級)時參考早期實現自行構建。