1. ReadWriteLock(讀寫鎖):實現高性能緩存
總結:
要點 | 內容 |
適用場景 | 讀多寫少、高并發讀取場景(如緩存) |
鎖類型 |
|
讀鎖 vs 寫鎖 | 多線程可同時讀,寫獨占 |
按需加載中的“二次檢查” | 避免重復查詢數據庫 |
鎖升級 | ? 不支持 |
鎖降級 | ? 支持(寫鎖降為讀鎖) |
數據一致性 | 可采用超時失效、Binlog 推送或雙寫策略 |
1.1. 讀寫鎖的概念
并發優化的場景:讀多寫少
- 實際開發中,緩存常用于提升性能(比如緩存元數據、基礎數據)
- 這類數據 讀取頻繁、寫入稀少,典型讀多寫少
常規鎖(互斥鎖)的限制
synchronized
或ReentrantLock
會限制所有線程串行訪問,即便是多個讀取操作- 性能瓶頸:多個讀線程也互相阻塞
ReadWriteLock的基本規則:
- 允許多個線程同時讀共享變量;
- 只允許一個線程寫共享變量;
- 如果一個寫線程正在執行寫操作,此時禁止讀線程讀共享變量。
Java 實現類:
- 接口:ReadWriteLock
- 實現:ReentrantReadWriteLock(支持可重入)
1.2. 封裝線程安全的緩存類
示例:Cache<K, V>
類(線程安全)
class Cache<K,V> {final Map<K, V> m = new HashMap<>();final ReadWriteLock rwl = new ReentrantReadWriteLock();final Lock r = rwl.readLock();final Lock w = rwl.writeLock();V get(K key) {r.lock();try { return m.get(key); }finally { r.unlock(); }}V put(String key, Data v) {w.lock();try { return m.put(key, v); }finally { w.unlock(); }}
}
緩存數據的加載策略
1. 一次性加載(適合數據量小)
- 程序啟動時從源頭加載所有數據,調用
put()
寫入緩存 - 簡單易行,示意圖如下:
2. 按需加載(懶加載,適合數據量大)
原理:
- 查詢緩存時,如果緩存中沒有數據,則從源頭加載并更新緩存
實現邏輯(含二次檢查):
V get(K key) {
V v = null;
r.lock(); // ① 獲取讀鎖
try { v = m.get(key); } // ② 嘗試從緩存讀取
finally { r.unlock(); } // ③ 釋放讀鎖if (v != null) return v; // ④ 緩存命中w.lock(); // ⑤ 獲取寫鎖
try {v = m.get(key); // ⑥ 再次檢查if (v == null) {v = 查詢數據庫(); // ⑦ 查詢源數據m.put(key, v); // 寫入緩存}
} finally {w.unlock(); // 釋放寫鎖
}
return v;
}
為什么要“再次驗證”?
- 防止多個線程同時 miss 緩存,導致重復數據庫查詢(讀寫鎖是排他的)
1.3. 讀寫鎖的升級與降級
1. 不支持鎖的升級
不允許持有讀鎖時再獲取寫鎖(會死鎖)
錯誤代碼示例:
r.lock();
try {if (m.get(key) == null) {w.lock(); // ? 升級為寫鎖,阻塞try { m.put(key, 查詢數據庫()); }finally { w.unlock(); }}
} finally {r.unlock(); // 死鎖
}
2. 支持鎖的降級
持有寫鎖時,可以先獲取讀鎖,再釋放寫鎖
w.lock(); // 寫鎖
try {if (!cacheValid) {data = 查詢數據();cacheValid = true;r.lock(); // 降級為讀鎖}
} finally {w.unlock(); // 釋放寫鎖
}try {use(data); // 仍持有讀鎖
} finally {r.unlock(); // 釋放讀鎖
}
補充:緩存一致性問題及解決方案
常見解決方式:
方式 | 描述 |
超時失效機制 | 每條緩存數據設定有效期,到期重新加載 |
Binlog 同步 | 數據庫變更觸發緩存更新(如 MySQL Binlog) |
數據雙寫 | 同時寫入緩存和數據庫(需解決一致性問題) |
2. StampedLock(比讀寫鎖更快)
StampedLock
提供 寫鎖、悲觀讀鎖、樂觀讀 三種模式;- 樂觀讀是 無鎖讀取 + 校驗機制,適合讀多寫少;
stamp
類似數據庫中的version
,用于一致性驗證;- 不支持重入、不支持條件變量、不支持中斷;
- 使用不當可能造成 CPU 飆升問題。
2.1. StampedLock的概念
背景與作用
- 傳統讀寫鎖(ReadWriteLock):適用于“讀多寫少”的場景,支持多個線程并發讀,但寫操作會阻塞所有讀操作。
- StampedLock(JDK 1.8 新增):
-
- 提供更高性能的讀寫控制機制;
- 特別適合讀多寫少場景;
- 支持 三種鎖模式,引入了性能更優的“樂觀讀”
StampedLock的三種鎖模式:
鎖類型 | 特點 | 互斥性 | 適用場景 |
寫鎖 | 和寫鎖類似 | 與所有其他鎖互斥 | 修改共享數據 |
悲觀讀鎖 | 與 ReadLock 類似,可多個線程同時持有 | 與寫鎖互斥 | 讀取共享數據(有一定寫的可能性) |
樂觀讀 | 無鎖!性能最好 | 可與寫鎖并發(需校驗) | 讀取頻繁,修改極少場景 |
- 加鎖后都會返回一個 stamp,釋放鎖時需要傳入。
代碼示例:
final StampedLock sl = new StampedLock();// 悲觀讀鎖
long stamp = sl.readLock();
try {// 讀取操作
} finally {sl.unlockRead(stamp);
}// 寫鎖
long stamp = sl.writeLock();
try {// 寫操作
} finally {sl.unlockWrite(stamp);
}
2.2. 樂觀讀原理與用法
樂觀讀流程:
- 調用
tryOptimisticRead()
獲取 stamp; - 讀取共享變量到局部變量(期間數據可能被其他線程寫操作修改!);
- 通過
validate(stamp)
判斷是否有寫操作發生;
-
- 若返回
true
,說明無寫操作,讀取有效; - 若返回
false
,則需“升級為悲觀讀鎖”。
- 若返回
示例代碼:
long stamp = sl.tryOptimisticRead();
int curX = x, curY = y;
if (!sl.validate(stamp)) {stamp = sl.readLock(); // 升級為悲觀讀try {curX = x;curY = y;} finally {sl.unlockRead(stamp);}
}
return Math.sqrt(curX * curX + curY * curY);
為什么比 ReadWriteLock 更快?
- 樂觀讀無鎖,不阻塞寫操作;
- 只有在檢測到寫入發生時,才升級為悲觀讀,大大減少了鎖競爭和阻塞。
使用注意事項
注意點 | 說明 |
? 不支持重入 |
|
? 不支持條件變量 | 不能用 |
? 不支持中斷 | 調用 |
對比數據庫樂觀鎖
- 數據庫中通過
version
字段實現樂觀鎖控制; - 讀取時返回
version
,更新時用where version=舊值
控制; - 與 StampedLock 的
stamp
機制非常相似,便于理解樂觀讀校驗的本質。
2.3. 使用模板
1. 讀操作模板:
long stamp = sl.tryOptimisticRead();
// 讀取局部變量
...
if (!sl.validate(stamp)) {stamp = sl.readLock();try {...} finally {sl.unlockRead(stamp);}
}// 使用局部變量...
2. 寫操作模板:
long stamp = sl.writeLock();
try {// 修改共享變量...
} finally {sl.unlockWrite(stamp);
}