在 Java Concurrency In Practice 中,提出了Computable<>
函数的缓存类,其中包含给定的示例代码:
public class Memorizer3<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memorizer3(Computable<A, V> c) { this.c = c; }
public V compute(final A arg) throws InterruptedException {
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 = ft;
cache.put(arg, ft);
ft.run(); // call to c.compute happens here
}
try {
return f.get();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}
然后继续解释这是几乎完美;从if(f == null)
开始的put-if-absent序列给出了一个微小但可能的窗口,当2个或更多并发线程可能观察到缓存不包含f
并同时尝试计算结果时,而不是等待对于单个线程。
为了解决这个问题,它建议使用putIfAbsent()
API提供的预定义ConcurrentHashMap
方法。这很有道理,但有一点可以证明它是令人费解的。
Memorizer3容易受到此问题的影响,因为在支持地图上执行了复合操作
(putIfAbsent)
无法使用锁定使其成为原子。
为什么会这样?我知道ConcurrentHashMap有自己的内部锁,因此尝试使用自身获取锁定不会阻止其他线程“恶意”进行更改。
然而,给出的当前示例遵循一些不同的模式,使得它似乎可以在客户端获得锁定(可能不是针对此特定用途,而是针对API不支持的任何其他复合操作)。
它不发布cache
,cache
实际上是不可变的,使得该对象完全被这个类封装。鉴于这种设置,是不是可以简单地使用客户端锁(使用对象cache
本身,比如说)Memorizer3
的类函数来实现原子性?
无论情况如何,基于对ConcurrentHashMaps的锁定实现原子性是否真的不可能,或者报价是否仅适用于某些条件?
*参考5.6。构建一个高效,可扩展的结果缓存,清单5.18。