Computable<A,V>接口中生命了一个函数Computable,其输入类型为A,输出类型为V,在ExpensiveFunction中实现的Computable,需要很长时间来计算结果,我们将创建一个Computable包装器,帮助记住之前的计算结果,并将缓存过程封装起来,(这项计算被称为“记忆(Memoization)”)

public interface Computable<A,V>{
V compute(A arg) throws InterruptedException;
} public class ExpensiveFunction implements Computable<String, BigInteger>{
public BigInteger compute(String arg){
//在经过长时间的计算后
return new BigInteger(arg);
}
} public class Memoizer1<A,V> implements Computable<A,V>{
@GuardeBy("this")
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;
}

在上述程序中的Memoizer1给出了第一种尝试,使用hashMap来保存之前计算的结果。compute方法将首先检查需要的结果是否已经在缓存中,如果存在则返回之前计算的值。否则,将把计算结果缓存在HashMap中,然后再返回。

HashMap不是线程安全的,因此要确保两个线程不会同时访问HashMap,Memoizer1采用了一种保守的方法,则将对整个compute方法进行同步,这种方法能确保线程安全性,但会带来一个很明显的可伸缩性问题,每次只有一个线程能够执行compute。如果另一个线程正在计算结果,那么其他调用compute的线程可能被阻塞很长时间。如果有多个线程在排队等待还未出结果,那这个缓存就没有了意义

优化步骤一

Memoizer2用ConcurrentHashMap代替HashMap来改进Memoizer1中糟糕的并发行为,由于ConcurrentHashMap是线程安全的,因此在访问底层Map时就不需要进行同步,因而避免了在对Memoizer1中的compute方法进行同步时带来的串行性。

Memoizer2比Memoizer1有着更好的并发行为,多线程可以并发使用它。但它在作为缓存时仍然存在一些不足:当两个线程同时调用compute时存在一个漏洞,可能会导致计算得到相同的值,即传入的key是一样的。在使用memoizatin的情况下,这只会带来抵消,因为缓存的作用是避免相同的数据被计算多次。但对于更通用的缓存机制来说,这种情况将更为糟糕,对于只提供单次初始化的对象缓存来说,这个漏洞就会带来安全风险。

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 compute(A arg) throws InterruptedException{
V result = cache.get(arg);
if(result == null){
result = c.compute(arg);
cache.put(arg,result);
}
result result;
}
}

Memoizer2的问题在于,如果某个线程启动了一个开销很大的计算,而其他线程并不知道这个计算正在进行,那么狠可能会重复这个计算,我们希望通过某种方法来表达“线程X正在计算 f(27)”这种情况,这样当另外一个线程查找f(27)时,它能够知道最高效的方法是等待线程X计算结束,然后再去查询缓存 “f(27)的结果是多少?”

我们已经知道有一个类能基本实现这个功能:FutureTask。 FutureTask表示一个计算的过程,这个过程可能已经计算完成,也有可能正在进行。如果有结果可用,那么FutureTask.get将立即返回结果,否则它会一直阻塞,知道结果计算出来再将其返回。

优化步骤二

下面的Memoizer3将用于缓存值的Map重新定义为ConcurrentHashMap<A, Future<V>>,替换原来的ComcurrentHashMap(A,V)。Memoizer3首先检查某个相应的计算是否已经开始(Memoizer2与之相反,它首先判断某个计算是否已经完成)。如果还没有启动,那么就将创建一个FutureTask,并注册到Map中,然后启动计算;如果已经启动,那么等待现有计算的结果,结果可能很快会得到,也可能还在运行过程中,但这对于Future.get的调用者来说是透明的。

public class Memoizer3<A,V> implements Computeable<A,V>{
private final Map<A,Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A,V> c;
public V compute(final A arg) throws InterruptedExceptin{
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){
}
}
}

Memoizer3的实现几乎是完美的,它表现了非常好的并发性(基本上是源于ConcurrentHashMap的并发性),若结果已经计算出来,那么将立即返回。如果其他线程正在计算结果,那么新到的线程就一直等待这个结果被计算出来。它只有一个缺陷,即仍然存在两个线程计算出相同值得漏洞,这个漏洞的发生概率要远小于Memoizer2中发生的概率,但由于compute方法中的if代码块仍然是非原子的“先检查再执行”操作,因此两个线程仍然有可能同一时间内调用compute来计算相同的值,即二者都没有在缓存中找到期望的值,因此都开始计算。

优化步骤四

MemoIzer3中存在这个问题的原因是,复合操作(“若没有则添加”)是在底层的Map对象上执行的,而这个对象无法通过加锁来确保原子性,下面的Memoizer使用了ConcurrentMap中的原子方法putIfAbsent,避免了Memoizer3的漏洞。

public class Memoizer<A,V> implements Computeable<A,V>{
private final Map<A,Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A,V> c;
public V compute(final A arg) throws InterruptedExceptin{
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);
cache.putIfAbsent(arg,ft);
if (f==null) {
f = ft;
ft.run(); //这里将调用c.compute
}
try{
return f.get();
}catch(ExecutionException e){
}catch(CancellationException e){
cache.remove(arg,f);
}
}
}

当缓存的是Future而不是值时,将导致缓存污染问题:如果某个计算被取消或者失败,那么在计算这个结果时将指明计算过程被取消或者失败。为了避免这种情况,如果Memoizer发现计算被取消,那么将把Future从缓存中移除。如果检测到RuntimeException,那么也会移除Future,这样将来的计算才可能成功。Memoizer同样没有解决缓存逾期的问题,但它可以通过使用FutureTask的子类来解决,在子类中为每个结果指定一个逾期时间,并定期扫描缓存中逾期的元素(同样,它也没有解决缓存清理的问题,即移除旧的计算结果以便为新的计算结果腾出空间,从而使缓存不会消耗过多的内存。)

关于Future,FutureTask,Callable的区别,参考http://blog.csdn.net/bboyfeiyu/article/details/24851847

关于cocurrentHashMap 参考http://www.infoq.com/cn/articles/ConcurrentHashMap

最新文章

  1. 一个技术汪的开源梦 —— 公共组件缓存之分布式缓存 Redis 实现篇
  2. 解决EBS中JAR包冲突的问题
  3. docker push 实现过程
  4. mysql “group by ”与&quot;order by&quot;的研究--分类中最新的内容
  5. 获取编辑框字符串,传入Intent
  6. VIM常用操作总结
  7. linphone3.4.0代码分析
  8. [转]EasyUI——常见用法总结
  9. [C++]KMP算法实现
  10. 最简单的jdbc操作
  11. k8s经典实战—搭建WordPress
  12. 2019-04-18 Beetl模板学习
  13. 【hdu 4658】Integer Partition (无序分拆数、五边形数定理)
  14. Shiro授权管理
  15. Android ANR Waiting because no window has focus问题分析
  16. Python学习 day04打卡
  17. Java中 Tomcat 是干什么的?
  18. M2C的概念
  19. 从Chrome 69.0 版本起,Flash权限受到进一步限制,默认仅在当前浏览器会话有效。
  20. 遍历XML文件

热门文章

  1. Android中Activity的四种启动方式
  2. MYSQL 存储过程、函数、临时表、游标
  3. Crack IDEA
  4. 钉钉开发笔记(六)使用Google浏览器做真机页面调试
  5. shiro 权限集成Ehcache 配置 学习记录(二)
  6. code1744 方格染色
  7. Solr开发文档(转)
  8. Qt的翻译文件QTranslator不能使用问题总结(原)
  9. python可视化
  10. T4模板调用反射