在高併發 Java 應用中,ReentrantReadWriteLock(RRW)是實現「多讀單寫」同步控制的核心工具。相較於 synchronized 或 ReentrantLock 的排他鎖,RRW 允許多執行緒同時讀取共享資料,僅在寫入時獨佔,大幅提升讀多寫少場景的吞吐量。本文深入解析 RRW 核心機制、鎖降級模式、常見陷阱,並以生產級快取系統為例示範實戰應用。
| 鎖類型 | 排斥寫鎖 | 排斥讀鎖 | 自身並發 |
|---|---|---|---|
| 讀鎖 | ✅ 是 | ❌ 否 | ✅ 多執行緒可同時持有 |
| 寫鎖 | ✅ 是 | ✅ 是 | ❌ 單執行緒獨佔 |
fair=true,避免寫執行緒飢餓。class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock(); // @1 獲取讀鎖
try {
if (!cacheValid) { // 快取失效
rwl.readLock().unlock(); // @4 必須先釋放讀鎖(升級前置條件)
rwl.writeLock().lock(); // @2 獲取寫鎖
try {
if (!cacheValid) { // @3 Double-Check:另一執行緒可能已更新
data = computeExpensiveData();
cacheValid = true;
}
} finally {
// 鎖降級:先獲取讀鎖,再釋放寫鎖
rwl.readLock().lock();
rwl.writeLock().unlock(); // 釋放寫鎖,保持讀鎖
}
}
use(data); // 讀取資料(持有讀鎖)
} finally {
rwl.readLock().unlock(); // 釋放讀鎖
}
}
}
// 錯誤!會導致死鎖
rwl.readLock().lock();
try {
rwl.writeLock().lock(); // 執行緒永遠阻塞:自己持有的讀鎖阻擋寫鎖
} finally { ... }
正確:先 readLock().unlock(),再 writeLock().lock()。
// 危險:多執行緒可能同時通過第一次檢查,重複執行昂貴計算
if (!cacheValid) {
rwl.readLock().unlock();
rwl.writeLock().lock();
// 缺少第二次檢查
data = compute(); // 可能重複計算
}
finally {
rwl.readLock().lock(); // 若這裡拋異常(罕見)
rwl.writeLock().unlock(); // 寫鎖永不釋放 → 系統癱瘓
}
建議:鎖操作放在 try-finally 外層,或確保 lock() 不拋異常(RRW lock() 不拋受檢異常)。
讀鎖應極短(記憶體讀取、快照複製)。若在讀鎖中執行 I/O、網路呼叫、複雜計算,會阻塞所有寫入者,降低系統回應性。
結合 HashMap、volatile、ReentrantReadWriteLock 實現執行緒安全、高效能的快取:
package com.example.cache;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 執行緒安全快取:讀多寫少場景優化
* - 讀操作並發無阻塞
* - 寫操作獨佔,雙重檢查避免重複計算
* - 鎖降級保證資料可見性
*/
public class CacheSystem {
private final Map cache = new HashMap<>();
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
private final Computable computable; // 昂貴計算介面
public interface Computable {
V compute(K key) throws Exception;
}
public CacheSystem(Computable computable) {
this.computable = computable;
}
/**
* 獲取資料:快取命中直接返回,未命中計算並存入
*/
public V get(K key) throws Exception {
// === 快速路徑:嘗試讀鎖獲取 ===
rwl.readLock().lock();
try {
V value = cache.get(key);
if (value != null) {
return value; // 快取命中,直接返回
}
} finally {
rwl.readLock().unlock();
}
// === 慢速路徑:需要計算 ===
rwl.writeLock().lock();
try {
// Double-Check:另一執行緒可能已在寫鎖中完成計算
V value = cache.get(key);
if (value == null) {
value = computable.compute(key); // 昂貴計算(持有寫鎖)
cache.put(key, value);
}
// 鎖降級:確保寫入對後續讀取者可見
rwl.readLock().lock();
rwl.writeLock().unlock();
return value;
} finally {
// 若未執行降級(如 compute 拋異常),確保釋放寫鎖
if (rwl.writeLock().isHeldByCurrentThread()) {
rwl.writeLock().unlock();
}
rwl.readLock().unlock(); // 釋放降級後的讀鎖
}
}
/** 手動失效快取 */
public void invalidate(K key) {
rwl.writeLock().lock();
try {
cache.remove(key);
} finally {
rwl.writeLock().unlock();
}
}
/** 清空快取 */
public void clear() {
rwl.writeLock().lock();
try {
cache.clear();
} finally {
rwl.writeLock().unlock();
}
}
}
// 定義昂貴計算:模擬資料庫查詢
CacheSystem.Computable dbQuery = key -> {
Thread.sleep(100); // 模擬延遲
return "Value-for-" + key;
};
CacheSystem cache = new CacheSystem<>(dbQuery);
// 併發測試
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final String key = "key-" + (i % 10); // 10 個不同 key,高命中率
es.submit(() -> {
try {
String val = cache.get(key);
System.out.println(Thread.currentThread().getName() + ": " + val);
} catch (Exception e) {
e.printStackTrace();
}
});
}
es.shutdown();
| 場景 | 建議 |
|---|---|
| 極高讀併發、極少寫入 | 使用 StampedLock (Java 8+) 乐观读模式,效能更高 |
| 需支援快取過期/淘汰 | 整合 Caffeine、Guava Cache 成熟庫,勿自造輪子 |
| 分散式環境 | 本地快取 + Redis 分散式快取,一致性透過 Pub/Sub 或 TTL 保證 |
| 寫入頻繁 | RRW 優勢消失,考慮 ConcurrentHashMap + computeIfAbsent 無鎖算法 |
StampedLock 提供三種模式:
StampedLock sl = new StampedLock();
// 樂觀讀
long stamp = sl.tryOptimisticRead();
V value = cache.get(key);
if (!sl.validate(stamp)) { // 有寫入發生,降級悲觀讀
stamp = sl.readLock();
try { value = cache.get(key); } finally { sl.unlockRead(stamp); }
}
// 寫入
long stamp = sl.writeLock();
try { cache.put(key, value); } finally { sl.unlockWrite(stamp); }
注意:StampedLock 不可重入,使用需更謹慎。
ReentrantReadWriteLock 是 Java 併發工具箱中處理「讀多寫少」共享資料的利器。掌握讀寫鎖互斥規則、鎖降級技巧、Double-Check 模式,並嚴格遵循 try-finally 釋放鎖的紀律,即可構建高效能、執行緒安全的快取與共享資料結構。現代專案中,優先評估 ConcurrentHashMap、StampedLock 或成熟快取庫(Caffeine),在確有必要時才手寫 RRW 邏輯。