`

并发与缓存——读《JCP》

 
阅读更多

缓存方法在我们编程中经常遇到。例如一个通过很复杂计算的值,但是一旦计算以后,就不再变化,我们可以用缓存存放。最简单的写法如下: 

Java代码  收藏代码
  1. Object value = null;  
  2. if ( (value = cache.get(key)) == null ) {  
  3.    value = compteValue(key);  
  4.   }  
  5. cache.put(key, value);  


如果不考虑多线程,这看起来也没什么问题。实际上的应用,则不太可能这样写,因为 
首先:缓存的出现,正是为了减少cpu的消耗,也就是说,compteValue这个方法应该是很耗时的(如果很简单就获取了,也没必要使用缓存)。其次,可能有多个线程并发访问这个缓存,也就是频繁使用,所以这里要说的也就是并发下的缓存。 
《JCP》中给出的例子循序渐进,非常好。我们也可以看到,concurrent包中的一些类的使用。下面我把这些例子放在这里,以供学习。 
Java代码  收藏代码
  1. public class Memoier1<K, V> implements Computable<K, V> {  
  2.     private final Map<K, V> cache = new HashMap<K, V>();  
  3.   
  4.     private final Computable<K, V> c;  
  5.   
  6.     public Memoier1(Computable<K, V> c) {  
  7.         this.c = c;  
  8.     }  
  9.   
  10.     @Override  
  11.     public synchronized V compute(K key) throws InterruptedException {  
  12.   
  13.         V result = cache.get(key);  
  14.         if (result == null) {  
  15.             result = c.compute(key);  
  16.             cache.put(key, result);  
  17.         }  
  18.         return result;  
  19.   
  20.     }  
  21. }  


很眼熟吧。虽然使用了 synchronized ,但是很不辛,这样的同步只会造成更低下的性能。因为synchronized 此时把整个方法锁住了。也就是每个调用线程只能排队调用吧。为了改善性能,我们想到了ConcurrentHashMap 
Java代码  收藏代码
  1. public class Memoier2<K, V> implements Computable<K, V> {  
  2.     private final Map<K, V> cache = new ConcurrentHashMap<K, V>();  
  3.   
  4.     private final Computable<K, V> c;  
  5.   
  6.     public Memoier1(Computable<K, V> c) {  
  7.         this.c = c;  
  8.     }  
  9.   
  10.     @Override  
  11.     public V compute(K key) throws InterruptedException {  
  12.   
  13.         V result = cache.get(key);  
  14.         if (result == null) {  
  15.             result = c.compute(key);  
  16.             cache.put(key, result);  
  17.         }  
  18.         return result;  
  19.   
  20.     }  
  21. }  

这下我们利用了ConcurrentHashMap的特性,compute方法可以并行了。可是,这依然不完美,因为我们说了,compute方法可能很耗时。也就是说,当一个线程在调用compute方法时,另外一个线程也开始调用,但是他不知道前一个线程的状态。所以他也调用了compute方法,这样,缓存就变得无意义了。我们只希望compute一次。问题就出在线程之间没有一个状态可共享。后面的线程如何知道这个key上又一个相同的线程正在执行呢? 
这是Future就派上用场了。Future可以捕获到线程执行的结果。 

Java代码  收藏代码
  1. import java.util.Map;  
  2. import java.util.concurrent.Callable;  
  3. import java.util.concurrent.CancellationException;  
  4. import java.util.concurrent.ConcurrentHashMap;  
  5. import java.util.concurrent.ExecutionException;  
  6. import java.util.concurrent.Future;  
  7. import java.util.concurrent.FutureTask;  
  8.   
  9. public class Memoier3<K,V> implements Computable<K,V>{  
  10.     private final Map<K,Future<V>> cache = new ConcurrentHashMap<K,Future<V>>();  
  11.     private final Computable<K, V> c;  
  12.       
  13.     public Memoier3(Computable<K,V> c){  
  14.         this.c = c;  
  15.     }  
  16.   
  17.     @SuppressWarnings("rawtypes")  
  18.     @Override  
  19.     public V compute(final K k) throws InterruptedException {  
  20.         // TODO Auto-generated method stub  
  21.         Future<V> f = cache.get(k);  
  22.         if(f == null){  
  23.              FutureTask<V> ft = new FutureTask<V>(new Callable<V>(){  
  24.   
  25.                 @Override  
  26.                 public V call() throws Exception {  
  27.                     return c.compute(k);   
  28.                 }  
  29.                    
  30.              });  
  31.              cache.put(k, ft);  
  32.              f = ft;  
  33.              ft.run();  
  34.         }     
  35.         try{  
  36.             return f.get();  
  37.         }catch(ExecutionException e){  
  38.             throw lanuderThrowable(e.getCause());  
  39.         }  
  40.     }  
  41.       
  42.     public static RuntimeException lanuderThrowable(Throwable t){  
  43.         if( t instanceof RuntimeException ){  
  44.             return (RuntimeException)t;  
  45.         }else if(t instanceof Error){  
  46.             throw new Error();  
  47.         }else  
  48.             throw new IllegalStateException("Not Unchecked", t);  
  49.           
  50.     }  
  51.   
  52. }  


这下看上去好多了。但是作者还是找出了一些问题来。首先,如果两个线程都访问compute函数,并且都处于 
Java代码  收藏代码
  1. if ( f == null ){  
  2.    ...  
  3.  put(k,v)  
  4. }  

这样的(check-then-act)的线程不安全中。其次,如果Future失败了,那么每次我们获取到的都是错误的结果(cache pollution),最后还有cache超时,新旧替换等问题(作者只是提一提,我们可以自己思考如何实现) 
下面是“最终”代码 
Java代码  收藏代码
  1. package book.jcp.basic;  
  2.   
  3. import java.util.Map;  
  4. import java.util.concurrent.Callable;  
  5. import java.util.concurrent.CancellationException;  
  6. import java.util.concurrent.ConcurrentHashMap;  
  7. import java.util.concurrent.ExecutionException;  
  8. import java.util.concurrent.Future;  
  9. import java.util.concurrent.FutureTask;  
  10.   
  11. public class Memoier<K, V> implements Computable<K, V> {  
  12.     private final ConcurrentHashMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>();  
  13.     private final Computable<K, V> c;  
  14.   
  15.     public Memoier(Computable<K, V> c) {  
  16.         this.c = c;  
  17.     }  
  18.   
  19.     @SuppressWarnings("rawtypes")  
  20.     @Override  
  21.     public V compute(final K k) throws InterruptedException {  
  22.         while (true) {  
  23.             Future<V> f = cache.get(k);  
  24.             if (f == null) {  
  25.                 FutureTask<V> ft = new FutureTask<V>(new Callable<V>() {  
  26.   
  27.                     @Override  
  28.                     public V call() throws Exception {  
  29.                         // TODO Auto-generated method stub  
  30.                         return c.compute(k);  
  31.                     }  
  32.   
  33.                 });  
  34.                 f = cache.putIfAbsent(k, ft);  
  35.                 if (f == null) {  
  36.                     f = ft;  
  37.                     ft.run();  
  38.                 }  
  39.             }  
  40.             try {  
  41.                 return f.get();  
  42.             } catch (CancellationException e) {  
  43.                 cache.remove(k);  
  44.             } catch (ExecutionException e) {  
  45.                 throw lanuderThrowable(e.getCause());  
  46.             }  
  47.         }  
  48.     }  
  49.   
  50.     public static RuntimeException lanuderThrowable(Throwable t) {  
  51.         if (t instanceof RuntimeException) {  
  52.             return (RuntimeException) t;  
  53.         } else if (t instanceof Error) {  
  54.             throw new Error();  
  55.         } else  
  56.             throw new IllegalStateException("Not Unchecked", t);  
  57.   
  58.     }  
  59.   
  60. }  


我们看到 ,解决第一个问题,使用了 putIfAbsent,使得这个点不会出现两次计算。而对于被取消(失败)的Future,则是采用捕获异常后移除Future来实现。 
最后提到的,缓存过期,就只能大家自己想办法了。 
我认为只能是添加一个租期参数,然后使用一个专门的线程来扫描缓存。
分享到:
评论

相关推荐

    java并发编程实战中文加英文版加源码

    本书作者都是Java Community Process JSR 166专家组(并发工具)的主要成员,并在其他很多JCP专家组里任职。Brian Goetz有20多年的软件咨询行业经验,并著有至少75篇关于Java开发的文章。Tim Peierls是“现代多...

    WLW_JCP2.zip

    标题 "WLW_JCP2.zip" 提供的信息表明这可能是一个关于WLW(可能是开发者或项目的简称)的项目,而“JCP2”可能是该项目的第二个版本或者特定部分的标识。描述简单地标注为“源文件”,暗示我们正在处理一个包含编程...

    JAVA并发编程实践.pdf

    本书作者系lava标准化组织(Java Cotl]munity Process)JSR 166专家组(并发工具)的主要成员,同时他们还致力于其他多个JCP专家组织。Brain Goetz是一位拥有二十年行业经验的软件咨询师,发表过超过75篇关于。Java开发...

    jcp:Java 并发实践

    Java 并发实践提炼 该存储库旨在存储组织在一本流行书籍讨论的想法、概念和问题的正在进行的工作的结果。 基本面 构建并发应用程序 活性、性能和测试 进阶课题 【Java内存模型】(the-java-memory-model.textile)

    Java并发编程实战

    本书作者都是Java Community Process JSR 166专家组(并发工具)的主要成员,并在其他很多JCP专家组里任职。Brian Goetz有20多年的软件咨询行业经验,并著有至少75篇关于Java开发的文章。 《Java并发编程实战》深入浅...

    杰表云打印 JCP 推出Webkit内核版

    杰表云打印 JCP 推出Webkit内核版,打印速度更快,功能更强 ! 1. 支持 CSS3 ,HTML 5 标签,如 Canvas,SVG; 2. 得力于js,渲染引擎速度提升,打印更快,对于JCP自动分页,速度提升近50%; 3. 对于同时使用国产系统...

    JCP035.py

    JCP035

    JCP007.py

    JCP007

    JCP011.py

    JCP011

    JCP073.py

    JCP073

    JCP002.py

    JCP002

    JCP071.py

    JCP071

    JCP060.py

    JCP060

    JCP057.py

    JCP057

    JCP048.py

    JCP048

    JCP072.py

    JCP072

    JCP008.py

    JCP008

    JCP010.py

    JCP010

    JCP022.py

    JCP022

    JCP001.py

    JCP001

Global site tag (gtag.js) - Google Analytics