You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

《Java并发编程实战》中Memoizer双重空检查的原因问询

关于Memoizer双重检查与ft.run()的解惑

先贴出你提到的Memoizer代码,方便对照分析:

public class Memoizer<A, V> implements Computable<A, V> {
    private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    public V compute(final A arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw launderThrowable(e.getCause());
            }
        }
    }
}

咱们逐个拆解你的疑问:

1. 为什么要做两次if(f == null)检查?

这是高并发场景下避免重复计算的核心设计,要结合ConcurrentHashMap.putIfAbsent的原子性来理解:

  • 第一次if(f == null):判断缓存中是否已有对应key的Future任务。如果没有,说明当前线程可能需要创建并提交计算任务。
  • 但并发环境下,可能有多个线程同时走到这个分支。比如线程A和线程B都发现缓存为空,线程A先执行putIfAbsent(arg, ft)——这个方法是原子操作,会把ft放入缓存并返回null(因为之前无此key)。而线程B随后执行putIfAbsent时,会返回线程A已经放入的ft(非null)。
  • 第二次if(f == null):用来区分「当前线程是第一个放入Future的发起者」还是「已有线程先一步提交了任务」。如果返回null,说明当前线程是第一个负责计算的,需要启动任务(也就是后面的ft.run());如果返回非null,说明已有线程在处理该key的计算,当前线程直接复用已有的Future即可,避免重复执行c.compute(arg)。

简单说,这两次检查配合原子性的putIfAbsent,保证了同一个key只会被计算一次,完美适配并发场景下的缓存需求。

2. ft.run()为什么不是无意义的?

你可能误解了FutureTask的工作机制:f.get()只会等待计算结果,但不会主动触发计算。

  • FutureTask有三种状态:未启动、运行中、已完成。如果不调用run(),它会一直处于未启动状态。
  • 此时调用f.get(),线程会无限阻塞,永远拿不到结果。
  • 这里的ft.run()是让第一个提交任务的线程,主动执行Callable里的c.compute(arg)逻辑。当计算完成后,所有调用get()的线程(包括当前线程和后续其他线程)才能获取到结果,实现「计算一次,复用多次」的缓存价值。

举个实际场景:线程A是第一个处理key X的线程,它创建ft并放入缓存,调用ft.run()启动计算。线程B随后请求key X,拿到缓存里的ft,调用get()就会等待线程A的计算完成,而不是自己再去执行一次c.compute(X)——这正是缓存要解决的核心问题。

内容的提问来源于stack exchange,提问作者Denis Wang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:47:48