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

【线程】结果缓存实现(future与concurrenthashmap)的更多相关文章

  1. Java线程池(Callable+Future模式)

    转: Java线程池(Callable+Future模式) Java线程池(Callable+Future模式) Java通过Executors提供四种线程池 1)newCachedThreadPoo ...

  2. RocksDB线程局部缓存

    概述 在开发过程中,我们经常会遇到并发问题,解决并发问题通常的方法是加锁保护,比如常用的spinlock,mutex或者rwlock,当然也可以采用无锁编程,对实现要求就比较高了.对于任何一个共享变量 ...

  3. Java线程池 / Executor / Callable / Future

    为什么需要线程池?   每次都要new一个thread,开销大,性能差:不能统一管理:功能少(没有定时执行.中断等).   使用线程池的好处是,可重用,可管理.   Executor     4种线程 ...

  4. java线程与缓存

    如果在你的服务中用了一些第三方的服务,最好使用缓存配合线程的方式去访问第三方的服务,以免引发线程安全问题,因为第三方的服务你不知道人家对于多线程是如何处理的,所以我们要在自己的程序中做一些线程安全的处 ...

  5. 【Java线程】Callable和Future

    Future模式 Future接口是Java线程Future模式的实现,可以来进行异步计算. Future模式可以这样来描述: 我有一个任务,提交给了Future,Future替我完成这个任务.期间我 ...

  6. Java程序员必须掌握的线程知识-Callable和Future

    Callable和Future出现的原因 创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口. 这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果. 如果需 ...

  7. java核心-多线程-线程类-Callable、Future和FutureTask

    基本概念 <1>Callable,Callable和Runnable差不多,两者都是为那些其实例可能被另一个线程执行的类而设计的,最主要的差别在于Runnable不会 返回线程运算结果,C ...

  8. (一)juc线程高级特性——volatile / CAS算法 / ConcurrentHashMap

    1. volatile 关键字与内存可见性 原文地址: https://www.cnblogs.com/zjfjava/category/979088.html 内存可见性(Memory Visibi ...

  9. Java - "JUC线程池" Callable与Future

    Java多线程系列--“JUC线程池”06之 Callable和Future Callable 和 Future 简介 Callable 和 Future 是比较有趣的一对组合.当我们需要获取线程的执 ...

随机推荐

  1. DVI与VGA有什么区别

    [DVI与VGA有什么区别] DVI接口的传输信号采用全数字格式,与之对应的是采用模拟信号的VGA接口. VGA和DVI的区别,首先VGA模拟信号的传输比较麻烦,首先是将电脑内的数字信号转换为模拟信号 ...

  2. Openssl ecparam命令

    一.简介 椭圆曲线密钥参数生成及操作 二.语法 openssl ecparam [-inform DER|PEM] [-outform DER|PEM] [-in filename] [-out fi ...

  3. lintcode-单例

    单例 是最为最常见的设计模式之一.对于任何时刻,如果某个类只存在且最多存在一个具体的实例,那么我们称这种设计模式为单例.例如,对于 class Mouse (不是动物的mouse哦),我们应将其设计为 ...

  4. java判断一个数是否为素数[转]

    http://blog.csdn.net/lwcumt/article/details/8027586 import java.util.Scanner; //质数又称素数,是指在一个大于1的自然数中 ...

  5. Perl 学习笔记-子程序

    1.定义子程序 使用sub关键字定义 ;   子程序名和标识符同要求, 但是有的特殊的可以用 &符号;  子程序是全局的, 不需要再使用前声明;  重名函数后者覆盖前者. sub roger{ ...

  6. UDP问题

    这两天使用C#的UdpClient,本机的服务是采用MFC的socket发的,用C#做客户端,然后客户端启动时,出现该条错误信息 ==通常每个套接字地址(协议/网络地址/端口)只允许使用一次. 笔记的 ...

  7. CodeForces 288B Polo the Penguin and Houses (暴力或都快速幂)

    题意:给定 n 和k,n 表示有n个房子,然后每个有一个编号,一只鹅要从一个房间中开始走,下一站就是房间的编号,现在要你求出有多少种方法编号并满足下面的要求: 1.如果从1-k房间开始走,一定能直到 ...

  8. msf、armitage

    msfconsole的命令: msfconsole use module :这个命令允许你开始配置所选择的模块. set optionname module :这个命令允许你为指定的模块配置不同的选项 ...

  9. (转)在ASP.NET MVC3 中利用Jsonp跨域访问

    原文地址:http://www.cnblogs.com/skm-blog/p/3431999.html 在信息系统开发的时,根据相关业务逻辑难免会多系统之间互相登录.一般情况下我们需要在多系统之间使用 ...

  10. 编写高质量代码改善C#程序的157个建议——建议112:将现实世界中的对象抽象为类,将可复用对象圈起来就是命名空间

    建议112:将现实世界中的对象抽象为类,将可复用对象圈起来就是命名空间 在我们身边的世界中,对象是什么?对象就是事物,俗称“东西”.那么,什么东西算得上是一个对象呢?对象有属性.有行为.以动物为例,比 ...