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. C#自省

    [C#自省] 1.根据string,获取type.Type.GetType 方法,获取具有指定名称的 Type,执行区分大小写的搜索. 2.根据obj,获取type.Object.GetType 方法 ...

  2. JAVA 中的IO流

    Java中的IO流是用来处理设备与设备之前的数据传输,在java中以流的形式传输.流分为两类:字节流和字符流. 字节流:InputStream,OutPutSteam.(计算机内的数据都是以字节存储的 ...

  3. java实现最通俗易懂的01背包问题

    这几天一直在想背包问题,昨天还有个学长专门讲了,但是还是不是很理解,今天我终于想通了背包问题,其实只要理解了这个思路,不管用什么语言,肯定是能编出来的.下面我就来介绍一下背包问题. 1.题目描述: 有 ...

  4. iOS调试程序的方法

    IOS各种调试技巧豪华套餐 普通操作 如图3 基本的断点操作如下 图4 点击那个黑列列就创建了一个断点,再次点击就临时取消这个断点(但是不删除),长按那个断点拖出去就删除了(mac os的系统工程师就 ...

  5. Fix: The account is not authorized to log in from this station

    If you have more the one computers running Windows, then its possible to connect them using HomeGrou ...

  6. HttpMessageConverter和ContentNegotiatingViewResolver

    HttpMessageConverter 在SpringMVC中,可以使用@RequestBody和@ResponseBody两个注解,分别完成请求报文到对象和对象到响应报文的转换,HttpMessa ...

  7. vue相关操作

    一: vue的安装 -安装node.js -vue脚手架 -vue create 项目名字 二:vue create 项目名字 用pycharm打开vue项目 -需要安装vue.js插件-settin ...

  8. [GO]使用select实现超时

    package main import ( "fmt" "time" ) func main() { ch := make(chan int) quit := ...

  9. C/C++语言中的函数参数传参三种对比

    学了很长时间C/C++有时指针方面还是有点乱. 希望大神发现如果下面有不对的地方请指出.我发现之所以我乱就是因为中文表述不准确的问题,比如 ,地址值和地址 #include <iostream& ...

  10. zrender源码分析3--初始化Painter绘图模块

    接上次分析到初始化ZRender的源码,这次关注绘图模块Painter的初始化 入口1:new Painter(dom, this.storage); // zrender.js /** * ZRen ...