含长耗时操作时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
相关产品推荐
相关产品推荐

