关闭

设计高效的线程安全的缓存(java并发编程实战5.6)

标签: 线程安全设计缓存应用
757人阅读 评论(0) 收藏 举报
分类:

几乎每一个应用都会使用到缓存, 但是设计高效的线程安全的缓存并不简单. 如:

Java代码  收藏代码
  1. public interface Computable<A, V> {   
  2.     V compute(A arg) throws InterruptedException;   
  3. }   
  4.   
  5. public class ExpensiveFunction   
  6.         implements Computable<String, BigInteger> {   
  7.     // 模拟一个耗时操作  
  8.     public BigInteger compute(String arg) {   
  9.     // ...  
  10.         return new BigInteger(arg);   
  11.     }   
  12. }   
  13.   
  14. public class Memorizer1<A, V> implements Computable<A, V> {   
  15.     private final Map<A, V> cache = new HashMap<A, V>();   
  16.     private final Computable<A, V> c;   
  17.   
  18.     public Memorizer1(Computable<A, V> c) {   
  19.         this.c = c;   
  20.     }   
  21.     // 使用synchronized同步整个方法解决线程安全  
  22.     public synchronized V compute(A arg) throws InterruptedException {   
  23.         V result = cache.get(arg);   
  24.         if (result == null) {   
  25.             result = c.compute(arg);   
  26.             cache.put(arg, result);   
  27.         }   
  28.         return result;   
  29.     }   
  30. }  

Memorizer1使用HashMap缓存计算结果. 如果能在缓存中取出参数对应的结果, 就直接返回缓存的数据, 避免了重复进行代价昂贵的计算. 由于HashMap不是线程安全的, Memorizer1同步整个compute方法, 避免重复计算的同时, 牺牲了并发执行compute方法的机会, 此种设计甚至可能导致性能比没有缓存更差.

使用ConcurrentHashMap代替HashMap, 同时取消对compute方法的同步可以极大的改善性能:

Java代码  收藏代码
  1. public class Memorizer2<A, V> implements Computable<A, V> {   
  2.     private final Map<A, V> cache = new ConcurrentHashMap<A, V>();   
  3.     private final Computable<A, V> c;   
  4.   
  5.     public Memorizer2(Computable<A, V> c) { this.c = c; }   
  6.   
  7.     public V compute(A arg) throws InterruptedException {   
  8.         V result = cache.get(arg);   
  9.         if (result == null) {   
  10.             result = c.compute(arg);   
  11.             cache.put(arg, result);   
  12.         }   
  13.         return result;   
  14.     }   
  15. }   

ConcurrentHashMap是线程安全的, 并且具有极好的并发性能. 但是该设计仍存在问题: 无法避免所有的重复的计算. 有时这是可以的, 但对于一些要求苛刻的系统, 重复计算可能会引发严重的问题. Memorizer2的问题在于一个线程在执行compute方法的过程中, 其他线程以相同的参数调用compute方法时, 无法从缓存中获知已有线程正在进行该参数的计算的信息, 因此造成了重复计算的发生. 针对这一点, 可以改进缓存的设计:

Java代码  收藏代码
  1. public class Memorizer3<A, V> implements Computable<A, V> {   
  2.     // 改为缓存Future  
  3.     private final Map<A, Future<V>> cache   
  4.             = new ConcurrentHashMap<A, Future<V>>();   
  5.     private final Computable<A, V> c;   
  6.   
  7.     public Memorizer3(Computable<A, V> c) { this.c = c; }   
  8.   
  9.     public V compute(final A arg) throws InterruptedException {   
  10.         Future<V> f = cache.get(arg);   
  11.         if (f == null) {   
  12.             Callable<V> eval = new Callable<V>() {   
  13.                 public V call() throws InterruptedException {   
  14.                     return c.compute(arg);   
  15.                 }   
  16.             };   
  17.             FutureTask<V> ft = new FutureTask<V>(eval);   
  18.             f = ft;   
  19.         // 在计算开始前就将Future对象存入缓存中.  
  20.             cache.put(arg, ft);   
  21.             ft.run(); // call to c.compute happens here   
  22.         }   
  23.         try {   
  24.         // 如果缓存中存在arg对应的Future对象, 就直接调用该Future对象的get方法.  
  25.         // 如果实际的计算还在进行当中, get方法将被阻塞, 直到计算完成  
  26.             return f.get();   
  27.         } catch (ExecutionException e) {   
  28.             throw launderThrowable(e.getCause());   
  29.         }   
  30.     }   
  31. }   

Memorizer3中的缓存系统看起来已经相当完美: 具有极好的并发性能, 也不会存在重复计算的问题. 真的吗? 不幸的是Memorizer3仍然存在重复计算的问题, 只是相对于Memorizer2, 重复计算的概率降低了一些. cache.get(arg)的结果为null, 不代表cache.put(arg, ft)时cache中依旧没有arg对应的Future, 因此直接调用cache.put(arg, ft)是不合理的:

Java代码  收藏代码
  1. public class Memorizer<A, V> implements Computable<A, V> {   
  2.     private final ConcurrentMap<A, Future<V>> cache   
  3.         = new ConcurrentHashMap<A, Future<V>>();   
  4.     private final Computable<A, V> c;   
  5.   
  6.     public Memorizer(Computable<A, V> c) { this.c = c; }   
  7.   
  8.     public V compute(final A arg) throws InterruptedException {   
  9.         while (true) {   
  10.             Future<V> f = cache.get(arg);   
  11.             if (f == null) {   
  12.                 Callable<V> eval = new Callable<V>() {   
  13.                     public V call() throws InterruptedException {   
  14.                         return c.compute(arg);   
  15.                     }   
  16.                 };   
  17.                 FutureTask<V> ft = new FutureTask<V>(eval);   
  18.         // 使用putIfAbsent测试是否真的将ft存入了缓存, 如果存入失败, 说明cache中已经存在arg对应的future对象  
  19.         // 否则才进行计算.  
  20.                 f = cache.putIfAbsent(arg, ft);   
  21.                 if (f == null) { f = ft; ft.run(); }   
  22.             }   
  23.             try {   
  24.                 return f.get();   
  25.             } catch (CancellationException e) {   
  26.         // 当计算被取消时, 从缓存中移除arg-f键值对  
  27.                 cache.remove(arg, f);   
  28.             } catch (ExecutionException e) {   
  29.                 throw launderThrowable(e.getCause());   
  30.             }   
  31.         }   
  32.     }   
  33. }   

至此才真正实现了高效且线程安全的缓存.

0
0
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

高吞吐、线程安全的LRU缓存

几年以前,我实现了一个LRU缓存用来为关键字来查找它的id。数据结构非常有意思,因为要求的吞吐很大足以消除大量使用locks和synchronized关键字带来的性能问题,应用是用java实现的。 我...
  • maoyeqiu
  • maoyeqiu
  • 2016-01-12 14:33
  • 1627

Java 高并发缓存与Guava Cache

一.背景 缓存是我们在开发中为了提高系统的性能,把经常的访问业务的数据第一次把处理结果先放到缓存中,第二次就不用在对相同的业务数据在重新处理一遍,这样就提高了系统的性能。缓存分好几种: (1)...
  • Michaelwubo
  • Michaelwubo
  • 2016-03-12 13:51
  • 11627

《Java并发编程实战》读书笔记-第5章 基础构建模块

第五章,基础构建模块 1,同步容器类。 Vector、HashTable此类的容器是同步容器。但也有一些问题,例如,一个线程在使用Vector的size()方法进行循环每一个元素的时候,而另一个线...
  • hotdust
  • hotdust
  • 2017-06-12 11:57
  • 180

设计一个缓存系统  java多线程读写锁的应用

高并发多线程读写锁的应用
  • u010959000
  • u010959000
  • 2016-03-23 01:37
  • 1508

Java的多线程机制:缓存一致性和CAS

一、总线锁定和缓存一致性 这是两个操作系统层面的概念。随着多核时代的到来,并发操作已经成了很正常的现象,操作系统必须要有一些机制和原语,以保证某些基本操作的原子性,比如处理器需要保证读一个字节或...
  • Zerohuan
  • Zerohuan
  • 2015-10-18 21:21
  • 1577

Java结果缓存实现(Thread Safe)

使用FutureTask实现:
  • a19881029
  • a19881029
  • 2014-07-10 15:19
  • 2822

java 线程实时更新缓存

java 线程实时更新缓存 废话不多说,直接上代码,大家看过,觉得和你做的功能相像,就拿去用吧,留不留言都不要紧,毕竟程序员不容易啊........... spring+jdbc框架 第一步:配置...
  • w2222288
  • w2222288
  • 2015-05-08 11:06
  • 2881

编写线程安全的Java缓存读写机制 (原创)

一种习以为常的缓存写法: IF value in cached THEN return value from cache ELSE compute value save va...
  • starcrm
  • starcrm
  • 2016-09-18 17:38
  • 1026

【4】Java并发编程:多线程中的缓存一致性和CAS

一、总线锁定和缓存一致性基本概念这是两个操作系统层面的概念。随着多核时代的到来,并发操作已经成了很正常的现象,操作系统必须要有一些机制和原语,以保证某些基本操作的原子性,比如处理器需要保证读一个字节或...
  • happy_horse
  • happy_horse
  • 2016-06-13 15:16
  • 4018

线程缓存的探索

线程通信有两种方式:共享内存与消息传递。 共享内存即多个线程共享程序的公共变量,通过变量状态的读写来进行隐式共享;消息传递则是线程之间没有公共变量,而是通过消息传递来进行显示的通信。而在Java中,...
  • u013769320
  • u013769320
  • 2015-10-05 15:09
  • 916
    个人资料
    • 访问:284908次
    • 积分:4145
    • 等级:
    • 排名:第8573名
    • 原创:101篇
    • 转载:100篇
    • 译文:0篇
    • 评论:28条
    最新评论