场景:假设某个计算过程需要耗费大量的时间,为了提高用户体验,希望将计算结果存入缓存,每次web端请求尝试从缓存中直接获取计算结果,如果缓存中没有,则计算并存入。
问题:这个需求的一大问题是,假设第一次请求发生后,程序正在运行计算函数,在未返回结果前,第二次请求发起,因为缓存中暂时没有存入第一次请求的计算结果,所以第二次请求依然会执行,因此必须加上同步策略。效率最低的解决方案是在获取缓存结果或计算的方法上加上synchronized
关键字,但这意味着同一时刻仅有一个线程可以执行该方法。本案例旨在构建高效的结果缓存。
一、定义通用计算接口
/**
* description: 计算接口 <br>
*/
public interface Computable<A, V> {
V compute(A arg) throws InterruptedException;
}
二、高效的结果缓存类
/**
* description: 线程安全的缓存类 <br>
* 尚缺乏解决缓存逾期或缓存清理的功能
*/
public class Memoizer<A, V> implements Computable<A, V> {
private final Computable<A, V> c;
// 使用Future避免多个线程同时进行重复消费
// 缓存Map的value由结果值 V 改为 Future<V>,这样判断任务是否存在即可,而无需计算完成
private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
public Memoizer(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(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 InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
// 使用ConcurrentMap的putIfAbsent方法避免两个以上的线程同时进行重复的计算
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {//若计算取消,将Future从缓存中移除,这样将来的计算才可能成功
cache.remove(arg, f);
} catch (ExecutionException e) {
throw ExceptionUtil.launderThrowable(e.getCause());
}
}
}
}
三、客户端示例
/**
* description: 使用缓存的客户端 <br>
*/
public class Factorizer implements Servlet {
private final Computable<BigInteger, BigInteger[]> c = arg -> factor(arg);
private final Computable<BigInteger, BigInteger[]> cache = new Memoizer<>(c);
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
try {
BigInteger i = extractFromRequest(req);
encodeIntoResponse(req, cache.compute(i));
} catch (InterruptedException e) {
encodeError(resp, "factorization interrupted");
}
}
/**
* 执行因式分解的操作
* @param arg
* @return
*/
private BigInteger[] factor(BigInteger arg) {
return new BigInteger[0];
}
}
参考文献
《Java并发编程实战》第五章 5.6构建高效且可伸缩的结果缓存