在高併發系統中,HashMap 作為輕量級緩存容器廣泛使用。但 HashMap 非線程安全,直接用於多線程環境會導致數據不一致、死循環(JDK 7 擴容)、數據丟失等嚴重問題。本文深度解析線程安全緩存實作模式,從 synchronized 到雙重檢查鎖(DCL),再到現代 ConcurrentHashMap 與 Caffeine 方案。
public class BasicCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
public V get(K key) {
synchronized (cache) {
return cache.get(key);
}
}
public V put(K key, V value) {
synchronized (cache) {
return cache.put(key, value);
}
}
public void clear() {
synchronized (cache) {
cache.clear();
}
}
}
缺點: 粗粒度鎖,讀寫互斥,高併發下吞吐率極低。
原文代碼展示的經典模式,適用於「計算成本高、結果可複用」場景:
public class DclCache<K, V> {
private final Map<K, V> cache = new HashMap<>();
public V get(K key, Supplier<V> loader) {
// 第一次檢查:無鎖快速路徑
V value = cache.get(key);
if (value != null) return value;
// 加鎖計算
synchronized (cache) {
// 第二次檢查:防止重複計算
value = cache.get(key);
if (value == null) {
value = loader.get(); // 昂貴操作:DB查詢、遠程調用、複雜計算
cache.put(key, value);
}
return value;
}
}
}
關鍵點:
volatile 不需要,因為 HashMap 內部結構修改在同步塊內public class ConcurrentCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
public V get(K key, Function<K, V> loader) {
// 原子性「查找或計算並放入」,JDK 8 保證 loader 只執行一次
return cache.computeIfAbsent(key, loader);
}
public void clear() { cache.clear(); }
public int size() { return cache.size(); }
}
優勢: 無鎖讀取、細粒度分段鎖(JDK 7)/ CAS + synchronized(JDK 8+)、API 簡潔、效能極佳。
手寫緩存終有極限(過期策略、淘汰算法、統計監控、異步加載)。Caffeine 是目前 Java 生態最高效的本地緩存庫:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
public class CaffeineCache<K, V> {
private final Cache<K, V> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats()
.build();
public V get(K key, Function<K, V> loader) {
return cache.get(key, k -> loader.apply(k));
}
public CacheStats stats() { return cache.stats(); }
}
| 方案 | 線程安全 | 效能 | 功能豐富度 | 適用場景 |
|---|---|---|---|---|
| HashMap + synchronized | ✅ | 低 | 無 | 教學、極低併發 |
| HashMap + DCL | ✅ | 中 | 基礎 | 單機、熱Key少、計算昂貴 |
| ConcurrentHashMap | ✅ | 高 | 基礎 | 高併發、無過期需求 |
| Caffeine | ✅ | 極高 | 完整(過期、淘汰、統計、異步) | 生產環境首選 |
| Redis | ✅ | 網絡開銷 | 分佈式、持久化、集群 | 多實例共享、大數據量 |
volatile 導致指令重排(單例模式),但本文 Map 場景鎖內初始化安全原文代碼展示的 DCL 模式在特定場景(單機、計算昂貴、讀多寫少)有參考價值,但現代 Java 開發強烈建議直接使用 ConcurrentHashMap.computeIfAbsent() 或 Caffeine 框架。手寫緩存僅作為學習併發原理之用,生產系統請站在巨人肩膀上,專注業務創新而非重造輪子。