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

含长耗时操作时computeIfAbsent的多线程行为及缓存实现疑问

《Java并发编程实战》Memoizer实现为何不用computeIfAbsent?

在《Java并发编程实战》中,作者逐步构建的线程安全缓存最终版实现如下:

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());
            }
        }
    }
}

该实现的核心是:当已有线程启动某个key的耗时计算时,其他线程会等待该计算完成,而非重复执行计算。

有开发者提出可以用computeIfAbsent简化实现,代码如下:

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) {
        cache.computeIfAbsent(arg, k -> c.compute(arg));
        return cache.get(arg);
    }
}

但原书作者没有采用这种方式,核心原因有三点:

  • Java版本兼容性限制:《Java并发编程实战》第一版出版于2006年,而ConcurrentHashMap的computeIfAbsent方法是Java 8(2014年)才新增的API,作者编写时根本没有这个方法可用。

  • 并发效率差异:简化版中,computeIfAbsent的函数式参数k -> c.compute(arg)是在调用线程中同步执行的。当多个线程竞争同一个key时,除了第一个线程能执行计算,其他线程会阻塞在computeIfAbsent方法调用上,直到计算完成;而原书实现中,其他线程会直接拿到已存在的Future,通过f.get()异步等待计算结果,不会占用调用线程同步阻塞,并发性能更优。

  • 异常与失败场景处理缺失:原书实现针对计算失败的场景做了完整处理:

    • 当计算被取消(CancellationException)时,会从缓存中移除对应的Future,后续线程可以重新发起计算;
    • 当计算抛出异常(ExecutionException)时,会通过launderThrowable处理底层异常并抛出,同时结合循环逻辑,后续线程可以重试计算。
      而简化版中,如果c.compute(arg)抛出异常,computeIfAbsent会直接传播异常,且不会将任何值存入缓存,导致后续线程每次调用都会重复执行失败的计算;同时也没有处理计算取消的场景,缓存中可能残留无效的Future。

内容的提问来源于stack exchange,提问作者Prashant Pandey

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 03:32:35