《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
设计不当(如未做防御性拷贝),则会破坏线程安全性。