缓存的简单实现

//此文基于《Java并发编程实践》

我们都知道在应用程序中合理地使用缓存,能更快的访问我们之前的计算结果,从而提高吞吐量。例如Redis和Memcached基于内存的数据存储系统等。此篇文章介绍如何实现简单缓存。

首先定义一个Computable接口A是输入,V是输出。

1 package simplecache;
2
3 /**
4  * Created by yulinfeng on 12/25/16.
5  */
6 public interface Computable<A, V> {
7     V compute(A arg) throws InterruptedException;
8 }

实现这个接口,也即是在ExpensiveFunction做具体的计算过程。

 1 package simplecache;
 2
 3 /**
 4  * Created by yulinfeng on 12/25/16.
 5  */
 6 public class ExpensiveFunction implements Computable<String, Integer> {
 7     @Override
 8     public Integer compute(String arg) throws InterruptedException {
 9         //计算
10         return new Integer(arg);
11     }
12 }

接着将创建一个Computable包装器,帮助记住之前的计算结果,并将缓存过程封装起来(Memoization)。

1.利用简单HashMap实现缓存

 1 package simplecache;
 2
 3 import java.util.HashMap;
 4 import java.util.Map;
 5
 6 /**
 7  * Created by yulinfeng on 12/25/16.
 8  */
 9 public class Memoizer1<A, V> implements Computable<A, V> {
10     private final Map<A, V> cache = new HashMap<A, V>();
11     private final Computable<A, V> c;
12
13     public Memoizer1(Computable<A, V> c){
14         this.c = c;
15     }
16
17     @Override
18     public synchronized V compute(A arg) throws InterruptedException {
19         V result = cache.get(arg);
20         if (null == result){
21             result = c.compute(arg);
22             cache.put(arg, result);
23         }
24         return result;
25     }
26 }

我们首先利用最简单的HashMap实现缓存,由于HashMap并不是线程安全的,所以在compute方法使用synchronized关键字,同步以实现线程安全。可见使用synchronized同步方法如此大粒度的同步必然会带来并发性的降低,因为每次只有一个线程执行compute方法,其余线程只能排队等待。

2.利用并发容器ConcurrentHashMap

第1种方法能实现缓存,且能实现线程安全的缓存,不过带来的问题就是并发性降低。我们使用并发包中的ConcurrentHashMap并发容器。

 1 package simplecache;
 2
 3 import java.util.Map;
 4 import java.util.concurrent.ConcurrentHashMap;
 5
 6 /**
 7  * Created by yulinfeng on 12/25/16.
 8  */
 9 public class Memoizer2<A, V> implements Computable<A, V> {
10     private final Map<A, V> cache = new ConcurrentHashMap<A, V>();
11     private final Computable<A, V> c;
12
13     public Memoizer2(Computable<A, V> c){
14         this.c = c;
15     }
16
17     @Override
18     public V compute(A arg) throws InterruptedException {
19         V result = cache.get(arg);
20         if (null == result){
21             result = c.compute(arg);
22             cache.put(arg, rsult);
23         }
24         return result;
25     }
26 }

毫无疑问,利用ConcurrentHashMap会比简单HashMap带来更好的并发性,同时它也是线程安全的。不过在有一种条件下,这种方式会带来一个新的问题,当这个计算过程比较复杂,计算时间比较长时,线程T1正在计算没有结束,此时线程T2并不知道此时T1已经在计算了,所以它同样会再次进行计算,这种条件下相当于一个值被计算了2次。我们应该想要达到的效果应该是T1正在计算,而此时T2能发现T1正在计算相同值,此时应该阻塞等待T1计算完毕返回计算结果,而不是T2也去做一次计算。FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。这正好我们想要达到的效果。

3.缓存的最佳实践——ConcurrentHashMap+FutureTask

 1 package simplecache;
 2
 3 import java.util.Map;
 4 import java.util.concurrent.ExecutionException;
 5 import java.util.concurrent.Future;
 6
 7 /**
 8  * Created by yulinfeng on 12/25/16.
 9  */
10 public class Memoizer3<A, V> implements Computable<A, V> {
11     private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
12     private final Computable<A, V> c;
13
14     public Memoizer3(Computable<A, V> c) {
15         this.c = c;
16     }
17
18     @Override
19     public V compute(final A arg) throws InterruptedException {
20         Future<V> f = cache.get(arg);
21         if (null == f){
22             Callable<V> eval = new Callable<V>() {
23                 @Override
24                 public V call() throws InterruptedException {
25                     return c.compute(arg);
26                 }
27             };
28             FutureTask<V> ft = new FutureTask<V>(eval);
29             cache.put(arg, ft);
30             ft.run();   //调用执行c.compute
31         }
32         try {
33             return f.get();
34         } catch (ExecutionException e) {
35             e.printStackTrace();
36         }
37     }
38 }

不了解FutureTask可以去补补了,但记住上面所说“FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。”,但这并不算是最完美的实现,在compute方法中出现了if的复合操作,也就是说在期间还是很有可能出现如同ConcurrentHashMap一样的重复计算,只是概率降低了而已。幸好,ConcurrentHashMap为我们提供了putIfAbsent的原子方法,从而完美的避免了这个问题。

 1 package simplecache;
 2
 3 import java.util.concurrent.*;
 4
 5 /**
 6  * Created by yulinfeng on 12/25/16.
 7  */
 8 public class Memoizer<A, V> implements Computable<A, V> {
 9     private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
10     private final Computable<A, V> c;
11
12     public Memoizer(Computable<A, V> c){
13         this.c = c;
14     }
15
16     @Override
17     public V compute(final A arg) throws InterruptedException {
18         while (true) {
19             Future<V> f = cache.get(arg);
20             if (null == f) {
21                 Callable<V> eval = new Callable<V>() {
22                     @Override
23                     public V call() throws Exception {
24                         return c.compute(arg);
25                     }
26                 };
27                 FutureTask<V> ft = new FutureTask<V>(eval);
28                 f = cache.putIfAbsent(arg, ft);
29                 if (null == f){
30                     f = ft;
31                     ft.run();
32                 }
33             }
34             try {
35                 return f.get();
36             } catch (CancellationException e){
37                 e.printStackTrace();
38             } catch (ExecutionException e) {
39                 e.printStackTrace();
40             }
41         }
42     }
43 }

这样我们利用ConcurrentHashMap的并发性已经putIfAbsent原子性,以及FutureTask的特性实现了一个简单缓存。

时间: 2024-10-22 09:33:58

缓存的简单实现的相关文章

Nginx专题: upstream模块和缓存的简单使用

Nginx专题: upstream模块和缓存的简单使用 前言: 本文接着上篇Nginx专题: 从编译安装到URL重写来介绍Nginx的负载均衡模块使用方法, 本文的实验没有考虑大多数情况, 例如两个web服务器之间的数据同步等, 主要写Nginx如何作为负载均衡器使用并且缓存 实验拓扑 实验环境 主机 IP地址 功用 lb.anyisalin.com 172.16.1.2 负载均衡并缓存静态资源 web1.anyisalin.com 172.16.1.3 提供web服务 web2.anyisal

使用NSURLProtocol实现UIWebView的离线缓存的简单实现

文章介绍了使用NSURLProtocol实现UIWebView的离线缓存的简单实现,你可以在github上下载这个demo的代码. 无论是"MKNetworkKit"还是"AFCache"实现的缓存都过于复杂,而他想要的是一个简单机制: 1.你使用了UIWebView指向来显示一个有图像嵌入的网站. 2.当你的设备online时,你有正常的缓存算法. 3.当你的设备offline时,你可以显示页面的最后一个版本. 这个demo里做了一个很简单的测试:将cnn.com

【Java/Android性能优 6】Android 图片SD卡缓存 使用简单 支持预取 支持多种缓存算法 支持不同网络类型 支持序列化

本文转自:http://www.trinea.cn/android/android-imagesdcardcache/ 本文主要介绍一个支持图片自动预取.支持多种缓存算法.支持数据保存和恢复的图片Sd卡缓存的使用.功能及网友反馈的常见问题解答. 需要二级缓存或ListView和GridView图片加载请优先使用ImageCache. 与Android LruCache相比主要特性:(1). 使用简单  (2). 轻松获取及预取新图片  (3). 可选择多种缓存算法(FIFO.LIFO.LRU.M

Mybatis缓存(1)--------系统缓存及简单配置介绍

前言 Mybatis的缓存主要有两种: 系统缓存,也就是我们一级缓存与二级缓存: 自定义的缓存,比如Redis.Enhance等,需要额外的单独配置与实现,具体日后主要学习介绍. 在这里主要记录系统缓存的一些简单概念, 并没有涉及原理.其中会涉及Mybatis的相关配置以及生命周期等. 主要参考资料:<深入浅出Mybatis基础原理与实战>,http://www.mybatis.org/mybatis-3/zh/index.html 1.Mybatis简单配置介绍 本文介绍的是基于XML的配置

缓存的简单实现方式

参考: <Java 并发实践> 李大狗大神的大作和源码非常值得分析阅读 1.String的hashCode:数据缓存在类变量中 字符串类维护了一个常量池-每当使用String str="xxx"创建对象,都会首先检查字符串是否在常量池中-有的话直接返回池中对象的实例引用,否则则创建一个对象返回并将对象放进池中. 没当调用 String.hashCode()方法时,字符串的hashCode便会缓存在String类变量中,下次查询是直接返回. 2.数据缓存在XXHashMap中

echache缓存的简单使用方法

1.需要echache的jar包 2.需要配置文件ehcache.xml和ehcache.xsd,主要是在ehcache.xml中进行配置 3.修改配置文件ehcache.xml  ,例如添加配置如下: <cache name="memoryCache" maxElementsInMemory="500" <!-- 最大缓存数量 --> eternal="true" <!-- 在内存中永久存在,由于此处设置为true,所以

缓存LruCache简单创建和使用

LruCache一般使用: /** * 总容量为当前进程的1/8,单位:KB * sizeOf():计算缓存对象的大小,单位要一致 * entryRemoved():移除旧缓存时调用 */ int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; LruCache lruCache = new LruCache<String, Bitmap>(cacheSize

使用Spring缓存的简单Demo

1. 首先创建Maven工程,在Pom中配置 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.1.2.RELEASE</version> </dependency> <dependency> <groupId>org.springfram

[转]缓存、缓存算法和缓存框架简单介绍

引言 我们都听过 cache,当你问他们是什么是缓存的时候,他们会给你一个完美的答案.可是他们不知道缓存是怎么构建的.或者没有告诉你应该採用什么标准去选择缓存框架. 在这边文章,我们会去讨论缓存.缓存算法.缓存框架以及哪个缓存框架会更好. 面试 "缓存就是存贮数据(使用频繁的数据)的暂时地方,由于取原始数据的代价太大了.所以我能够取得快一些." 这就是 programmer one (programmer one 是一个面试者)在面试中的回答(一个月前,他向公司提交了简历,想要应聘要求