最近重新翻看《java并发编程实战》,现在读这本书还是挺轻松的。
然而不得不说,java并发的坑儿还是太多了,不小心写出的程序要么伸缩性不够,要么安全性问题或者活跃性问题。
譬如这个构建缓存的例子。
先看,第一个伸缩性极差的代码
public class Memoizer1<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new HashMap<A, V>();
private final Computable<A, V> c;
public Memoizer1(Computable<A, V> c) {
this.c = c;
}
@Override
public synchronized V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}
这里的伸缩性问题在于:使用内置锁的compute方法如果包含长时间的计算过程,那么调用compute方法的线程会阻塞时间过长。
然后,看最终版本
public class Memoizer<A, V> implements Computable<A, V> {
private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizer(Computable<A, V> c) {
this.c = c;
}
@Override
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>() {
@Override
public V call() throws Exception {
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 (ExecutionException e) {
e.printStackTrace();
}
return null;
}
}
}
这个版本使用线程安全的ConcurrentHashMap代替了HashMap,然后使用Future和Callable延迟了实际的计算过程,注意这里 使用 f=cache.putIfAbsent(arg,ft)代替了cache.put(arg,ft),这里是由于compute方法的if代码块是非原子的“先检查再执行”操作。