缓存的简单实现
2016-12-25 20:32
288 查看
//此文基于《Java并发编程实践》
我们都知道在应用程序中合理地使用缓存,能更快的访问我们之前的计算结果,从而提高吞吐量。例如Redis和Memcached基于内存的数据存储系统等。此篇文章介绍如何实现简单缓存。
首先定义一个Computable接口A是输入,V是输出。
实现这个接口,也即是在ExpensiveFunction做具体的计算过程。
接着将创建一个Computable包装器,帮助记住之前的计算结果,并将缓存过程封装起来(Memoization)。
1.利用简单HashMap实现缓存
我们首先利用最简单的HashMap实现缓存,由于HashMap并不是线程安全的,所以在compute方法使用synchronized关键字,同步以实现线程安全。可见使用synchronized同步方法如此大粒度的同步必然会带来并发性的降低,因为每次只有一个线程执行compute方法,其余线程只能排队等待。
2.利用并发容器ConcurrentHashMap
第1种方法能实现缓存,且能实现线程安全的缓存,不过带来的问题就是并发性降低。我们使用并发包中的ConcurrentHashMap并发容器。
毫无疑问,利用ConcurrentHashMap会比简单HashMap带来更好的并发性,同时它也是线程安全的。不过在有一种条件下,这种方式会带来一个新的问题,当这个计算过程比较复杂,计算时间比较长时,线程T1正在计算没有结束,此时线程T2并不知道此时T1已经在计算了,所以它同样会再次进行计算,这种条件下相当于一个值被计算了2次。我们应该想要达到的效果应该是T1正在计算,而此时T2能发现T1正在计算相同值,此时应该阻塞等待T1计算完毕返回计算结果,而不是T2也去做一次计算。FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。这正好我们想要达到的效果。
3.缓存的最佳实践——ConcurrentHashMap+FutureTask
不了解FutureTask可以去补补了,但记住上面所说“FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。”,但这并不算是最完美的实现,在compute方法中出现了if的复合操作,也就是说在期间还是很有可能出现如同ConcurrentHashMap一样的重复计算,只是概率降低了而已。幸好,ConcurrentHashMap为我们提供了putIfAbsent的原子方法,从而完美的避免了这个问题。
这样我们利用ConcurrentHashMap的并发性已经putIfAbsent原子性,以及FutureTask的特性实现了一个简单缓存。
我们都知道在应用程序中合理地使用缓存,能更快的访问我们之前的计算结果,从而提高吞吐量。例如Redis和Memcached基于内存的数据存储系统等。此篇文章介绍如何实现简单缓存。
首先定义一个Computable接口A是输入,V是输出。
package simplecache; /** * Created by yulinfeng on 12/25/16. */ public interface Computable<A, V> { V compute(A arg) throws InterruptedException; }
实现这个接口,也即是在ExpensiveFunction做具体的计算过程。
package simplecache; /** * Created by yulinfeng on 12/25/16. */ public class ExpensiveFunction implements Computable<String, Integer> { @Override public Integer compute(String arg) throws InterruptedException { //计算 return new Integer(arg); } }
接着将创建一个Computable包装器,帮助记住之前的计算结果,并将缓存过程封装起来(Memoization)。
1.利用简单HashMap实现缓存
package simplecache; import java.util.HashMap; import java.util.Map; /** * Created by yulinfeng on 12/25/16. */ public class Memoizer1<A, V> implements Computable<A, V> { 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; } @Override public synchronized V compute(A arg) throws InterruptedException { V result = cache.get(arg); if (null == result){ result = c.compute(arg); cache.put(arg, result); } return result; } }
我们首先利用最简单的HashMap实现缓存,由于HashMap并不是线程安全的,所以在compute方法使用synchronized关键字,同步以实现线程安全。可见使用synchronized同步方法如此大粒度的同步必然会带来并发性的降低,因为每次只有一个线程执行compute方法,其余线程只能排队等待。
2.利用并发容器ConcurrentHashMap
第1种方法能实现缓存,且能实现线程安全的缓存,不过带来的问题就是并发性降低。我们使用并发包中的ConcurrentHashMap并发容器。
package simplecache; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Created by yulinfeng on 12/25/16. */ 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; } @Override public V compute(A arg) throws InterruptedException { V result = cache.get(arg); if (null == result){ result = c.compute(arg); cache.put(arg, rsult); } return result; } }
毫无疑问,利用ConcurrentHashMap会比简单HashMap带来更好的并发性,同时它也是线程安全的。不过在有一种条件下,这种方式会带来一个新的问题,当这个计算过程比较复杂,计算时间比较长时,线程T1正在计算没有结束,此时线程T2并不知道此时T1已经在计算了,所以它同样会再次进行计算,这种条件下相当于一个值被计算了2次。我们应该想要达到的效果应该是T1正在计算,而此时T2能发现T1正在计算相同值,此时应该阻塞等待T1计算完毕返回计算结果,而不是T2也去做一次计算。FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。这正好我们想要达到的效果。
3.缓存的最佳实践——ConcurrentHashMap+FutureTask
package simplecache; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; /** * Created by yulinfeng on 12/25/16. */ public class Memoizer3<A, V> implements Computable<A, V> { private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>(); private final Computable<A, V> c; public Memoizer3(Computable<A, V> c) { this.c = c; } @Override public V compute(final A arg) throws InterruptedException { Future<V> f = cache.get(arg); if (null == f){ Callable<V> eval = new Callable<V>() { @Override public V call() throws InterruptedException { return c.compute(arg); } }; FutureTask<V> ft = new FutureTask<V>(eval); cache.put(arg, ft); ft.run(); //调用执行c.compute } try { return f.get(); } catch (ExecutionException e) { e.printStackTrace(); } } }
不了解FutureTask可以去补补了,但记住上面所说“FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。”,但这并不算是最完美的实现,在compute方法中出现了if的复合操作,也就是说在期间还是很有可能出现如同ConcurrentHashMap一样的重复计算,只是概率降低了而已。幸好,ConcurrentHashMap为我们提供了putIfAbsent的原子方法,从而完美的避免了这个问题。
package simplecache; import java.util.concurrent.*; /** * Created by yulinfeng on 12/25/16. */ public class Memoizer<A, V> implements Computable<A, V> { private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>(); private final Computable<A, V> c; public Memoizer(Computable<A, V> c){ this.c = c; } @Override public V compute(final A arg) throws InterruptedException { while (true) { Future<V> f = cache.get(arg); if (null == f) { Callable<V> eval = new Callable<V>() { @Override public V call() throws Exception { return c.compute(arg); } }; FutureTask<V> ft = new FutureTask<V>(eval); f = cache.putIfAbsent(arg, ft); if (null == f){ f = ft; ft.run(); } } try { return f.get(); } catch (CancellationException e){ e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } }
这样我们利用ConcurrentHashMap的并发性已经putIfAbsent原子性,以及FutureTask的特性实现了一个简单缓存。
相关文章推荐
- .NET简单缓存的快速实现和分析
- 简单实现缓存需求
- javaEE 使用ServletContext实现服务器端简单定时更新缓存
- 利用LinkedHashMap简单实现基于LRU策略的缓存
- 简单LRU算法实现缓存
- 使用Hashtable实现简单缓存功能
- 关于Session缓存简单原理和实现
- Java实现简单的LRU缓存(A Simple LRU Cache in 5 lines)
- java缓存的简单实现
- Java中读写锁的实现及使用读写锁简单实现缓存系统的实例
- 简单实现缓存需求
- 简单LRU算法实现缓存
- 一个简单的JavaScript数据缓存系统实现代码
- Java实现简单的LRU缓存(A Simple LRU Cache in 5 lines)
- java中读写锁的实现及使用读写锁简单实现缓存系统的实例
- 简单实现缓存需求
- 一个简单的JavaScript数据缓存系统实现代码
- DNS服务器概念的简单的介绍,与搭建一个简单的DNS名称缓存服务器,实现域名解析(一)
- Lru缓存的简单实现
- 缓存的简单实现