《Java并發編程實戰》中的VolatileCachedFactorizer
展示了如何使用volatile
和不可變性來實現線程安全。解決了簡單緩存實現中可能出現的線程安全問題,同時避免了全量同步帶來的性能開銷。
場景背景
假設有一個服務(如因數分解服務),需要緩存最近的計算結果以提高效率:
- 當新請求的參數與緩存中的參數相同時,直接返回緩存結果。
- 當參數不同時,重新計算并更新緩存。
核心挑戰:如何在多線程并發訪問時,保證緩存讀寫的線程安全,同時減少同步開銷。
代碼實現與核心思路
VolatileCachedFactorizer
的關鍵實現如下:
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {// 用volatile修飾緩存的"不可變結果對象"private volatile ImmutableCache cache = new ImmutableCache(null, null);@Overridepublic void service(ServletRequest req, ServletResponse resp) {BigInteger i = extractFromRequest(req);BigInteger[] factors = cache.getFactors(i);// 緩存未命中,重新計算并更新緩存if (factors == null) {factors = factor(i);// 創建新的不可變對象替換舊緩存cache = new ImmutableCache(i, factors);}encodeIntoResponse(resp, factors);}// 不可變的緩存對象private static class ImmutableCache {private final BigInteger lastNumber;private final BigInteger[] lastFactors;public ImmutableCache(BigInteger lastNumber, BigInteger[] lastFactors) {this.lastNumber = lastNumber;// 防御性拷貝,避免外部修改內部數組this.lastFactors = lastFactors != null ? Arrays.copyOf(lastFactors, lastFactors.length) : null;}// 檢查緩存是否命中public BigInteger[] getFactors(BigInteger i) {if (lastNumber == null || !lastNumber.equals(i)) {return null;}// 返回拷貝,避免外部修改內部狀態return Arrays.copyOf(lastFactors, lastFactors.length);}}// 其他輔助方法(提取參數、因數分解、編碼響應)private BigInteger extractFromRequest(ServletRequest req) { ... }private BigInteger[] factor(BigInteger i) { ... }private void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { ... }
}
線程安全的核心設計
1. 不可變對象消除了 “寫沖突”
ImmutableCache
是不可變的(所有成員變量用final
修飾,且無修改方法):
- 一旦創建,其內部狀態(
lastNumber
和lastFactors
)就無法被修改。 - 任何 “更新緩存” 的操作,本質上都是創建一個新的
ImmutableCache
對象,而非修改原有對象。
這就從根本上避免了多線程同時修改同一對象的問題 —— 因為根本沒有 “修改” 行為,只有 “替換” 對象引用的操作。
2. volatile 保證了 “讀可見性”
cache
變量用volatile
修飾,確保了:
- 當一個線程創建新的
ImmutableCache
并賦值給cache
時,這個更新會被立即同步到主內存。 - 其他線程讀取
cache
時,會從主內存獲取最新值,而非使用本地緩存的舊值。
因此,線程不會讀取到 “過期” 的緩存對象,保證了共享狀態的可見性。
3. 無鎖設計避免了 “同步競爭”
與synchronized
等鎖機制不同,這個實現:
- 讀取緩存時完全無鎖,多個線程可以同時安全訪問
cache
(因為對象不可變,讀操作本身不會有沖突)。 - 更新緩存時僅通過 “創建新對象 + 替換引用” 實現,這個操作是原子的(引用賦值在 Java 中是原子操作)。
雖然可能出現 “多個線程同時計算并覆蓋緩存” 的情況(導致臨時的重復計算),但這種情況不會破壞線程安全 —— 最終緩存會是某個線程計算的正確結果,且所有線程最終都會看到這個最新結果。
可能的問題與局限性
- 緩存覆蓋問題:如果兩個線程同時發現緩存未命中,會同時計算并先后更新緩存,后更新的結果會覆蓋先更新的,可能導致短暫的“緩存失效”(但不影響線程安全,只是效率略有損失)。
- 不適合復雜緩存邏輯:僅適用于“單鍵單值”的簡單緩存場景,無法處理緩存過期、LRU淘汰等復雜策略。
- 依賴不可變性:若
ImmutableCache
設計不當(如未做防御性拷貝),則會破壞線程安全性。