为计算结果建立高效、可伸缩的高速缓存
public interface Computable<A,V>{
V compute(A arg)throws InterruptedException;
}
public class ExpensiveFunction implements Computable<String,BigInteger>{
public BigInteger compute(String arg){
// after deep thought
return new BigInteger(arg);
}
}
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;
}
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;
}
}
下面是一种改进。
public class Memoizer2<A,V> implements Computable<A,V>{
private final Map<A,V> cache=new ConcurrentHashMap<A,V>();
private final Computable<A,V> c;
public Memoizer2(Computable<A,V> c){
this.c=c;
}
public V comput(A arg)throws InterruptedException{
V result=cache.get(arg);
if(result==null){
result=c.compute(arg);
cache.put(arg,result);
}
return result;
}
}
但是上面的代码依然存在缺陷,当两个线程同时调用compute 时,会造成他们计算相同的值。
public class Memoizer3<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 Memoizer3(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();//调用c.compute发生在这里
}
try{
return f.get();
}catch(ExecutionException e){
throw launderThrowable(e.getCause());
}
}
}
上面的例子近乎完美,只存在一个缺陷——两个线程同时计算相同的值,但是远没有 Memoizer2 的严重,仅仅因为 compute 中的 if 代码块是非原子的检查再运行。
缓存一个Future而不是一个值会带来缓存污染的可能性:如果一个计算被取消或者失败,未来尝试对这个值计算都会失败,所以如果计算被取消,就会把Future从缓存中移除。发现异常的时候也会移除。缓存过期的问题可以通过FutureTask的一个子类来完成,他会为每一个结果关联一个过期时间,并周期性地扫描缓存中过期的访问。
下面是Memoizer的最终实现。
public class Memoizer<A,V> implements Computable<A,V>{
private final ConcurrentMap<A,Future<V>> cache = new ConcurrentMap<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());
}
}
}
}
使用上面的代码分解因式
public class Factorizer implements Servlet{
private final Computable<BigInteger,BigInteger[]> c = new Computable<BigInteger,BigInteger[]>(){
public BigInteger[] compute(BigInteger arg){
return factor(arg);
}
};
private final Computable<BigInteger,BigInteger[]> cache = new Memoizer<BigInteger,BigInteger[]>(c);
public void service(ServletRequest req,ServletResponse resp){
try{
BigInteger i=extractFromRequest(req);
encodeIntoResponse(resp,cache.compute(i));
}catch(InterruptedException e){
encodeError(resp,”factorization interrupted”);
}
}
}
下面我们总结一下
可变状态,所有并发问题都归结为如何协调访问并发状态,可变状态越少,保证线程安全就越容易。
尽量将域声明为final类型,除非他们需要是可变的。
不可变的对象是线程安全的。
封装使得管理复杂度变得更可行。
用锁来守护每一个可变变量。
对同一不变约束中的所有变量都使用相同的锁。
在运行负荷操作期间持有锁。
在非同步的多线程情况下,访问可变变量的程序是存在隐患的。
不要依赖于可以需要同步的小聪明。
在设计过程中就考虑线程安全,或者在文档中明确地说明他不是线程安全的。
文档化你的同步策略。