《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
相关产品推荐
相关产品推荐

