并发:使用volatile和不可变性实现线程安全

发布于:2025-09-13 ⋅ 阅读:(18) ⋅ 点赞:(0)

《Java并发编程实战》中的VolatileCachedFactorizer展示了如何使用volatile和不可变性来实现线程安全。解决了简单缓存实现中可能出现的线程安全问题,同时避免了全量同步带来的性能开销。

场景背景

假设有一个服务(如因数分解服务),需要缓存最近的计算结果以提高效率:

  • 当新请求的参数与缓存中的参数相同时,直接返回缓存结果。
  • 当参数不同时,重新计算并更新缓存。

核心挑战:如何在多线程并发访问时,保证缓存读写的线程安全,同时减少同步开销

代码实现与核心思路

VolatileCachedFactorizer的关键实现如下:

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
    // 用volatile修饰缓存的"不可变结果对象"
    private volatile ImmutableCache cache = new ImmutableCache(null, null);

    @Override
    public 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修饰,且无修改方法):

  • 一旦创建,其内部状态(lastNumberlastFactors)就无法被修改。
  • 任何 “更新缓存” 的操作,本质上都是创建一个新的ImmutableCache对象,而非修改原有对象。

这就从根本上避免了多线程同时修改同一对象的问题 —— 因为根本没有 “修改” 行为,只有 “替换” 对象引用的操作。

2. volatile 保证了 “读可见性”

cache变量用volatile修饰,确保了:

  • 当一个线程创建新的ImmutableCache并赋值给cache时,这个更新会被立即同步到主内存
  • 其他线程读取cache时,会从主内存获取最新值,而非使用本地缓存的旧值。

因此,线程不会读取到 “过期” 的缓存对象,保证了共享状态的可见性。

3. 无锁设计避免了 “同步竞争”

synchronized等锁机制不同,这个实现:

  • 读取缓存时完全无锁,多个线程可以同时安全访问cache(因为对象不可变,读操作本身不会有冲突)。
  • 更新缓存时仅通过 “创建新对象 + 替换引用” 实现,这个操作是原子的(引用赋值在 Java 中是原子操作)。

虽然可能出现 “多个线程同时计算并覆盖缓存” 的情况(导致临时的重复计算),但这种情况不会破坏线程安全 —— 最终缓存会是某个线程计算的正确结果,且所有线程最终都会看到这个最新结果。

可能的问题与局限性

  • 缓存覆盖问题:如果两个线程同时发现缓存未命中,会同时计算并先后更新缓存,后更新的结果会覆盖先更新的,可能导致短暂的“缓存失效”(但不影响线程安全,只是效率略有损失)。
  • 不适合复杂缓存逻辑:仅适用于“单键单值”的简单缓存场景,无法处理缓存过期、LRU淘汰等复杂策略。
  • 依赖不可变性:若ImmutableCache设计不当(如未做防御性拷贝),则会破坏线程安全性。