【转】基于Map的简易记忆化缓存
看到文章后,自己也想写一些关于这个方面的,但是觉得写的估计没有那位博主好,而且又会用到里面的许多东西,所以干脆转载。但是会在文章末尾写上自己的学习的的东西。
原文出处如下:
http://www.cnblogs.com/micrari/p/6921661.html
背景
在应用程序中,时常会碰到需要维护一个map,从中读取一些数据避免重复计算,如果还没有值则计算一下塞到map里的的小需求(没错,其实就是简易的缓存或者说实现记忆化)。在公司项目里看到过有些代码中写了这样简易的缓存,但又忽视了线程安全、重复计算等问题。本文主要就是谈谈这个小需求的实现。
实现
HashMap的实现
在公司项目里看到过有类似如下的代码。
public class SimpleCacheDemo {
private Map<Integer, Integer> cache = new HashMap<>();
public synchronized Integer retrieve(Integer key) {
Integer result = cache.get(key);
if (result == null) {
result = compute(key);
cache.put(value,result);
}
return result;
}
private Integer compute(Integer key) {
// 模拟代价很高的计算
return key;
}
}
只是那位同事写的代码比这段代码更糟,连synchronized关键字都没加。
这段代码的问题还在于由于在compute方法上进行了同步,所以大大降低了并发性,在具体场景中,如果compute代价很高,那么其他线程会长时间阻塞。
基于ConcurrentHashMap的改进
一种改进的策略是将上述map的实现类替换为ConcurrentHashMap并去除compute上的synchronized。这样可以规避在compute上同步带来的伸缩性问题。
但与上面的方法一样还有一个问题在于,由于compute的耗时可能不少,在另一个线程读到map中还没有值时可能同样会开始进行计算,这样就出现了重复高代价计算的问题。
基于Future的改进
为了规避重复计算的问题,可以将map中的值类型用Future封起来。代码如下:
public class SimpleCacheDemo {
private Map<Integer, Future<Integer>> cache = new HashMap<>();
public Integer retrieve(Integer key) {
Future<Integer> result = cache.get(key);
if (result == null) {
FutureTask<Integer> task = new FutureTask<>(() -> compute(key));
cache.put(key, task);
result = task;
task.run();
}
try {
return result.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
private Integer compute(Integer value) {
// 模拟代价很高的计算
return value;
}
}
当在map中读取到result为null时,建一个FutureTask塞到map并进行计算,最后获取结果。但实际上这样的实现仍然有可能出现重复计算的问题,问题在于判断map中是否有值,无值则插入的操作是一个复合操作。上面的代码中这样的无则插入的复合操作既不是原子的,也没有同步。
putIfAbsent
上面的问题无非就只剩下了无则插入这样的先检查后执行的操作不是原子的也没有同步。
事实上,解决的方法很简单,在JDK8中Map提供putIfAbsent,也即若没有则插入的方法。本身是不保证原子性、同步性的,但是在ConcurrentHashMap中的实现是具有原子语义的。我们可以将上面的代码再次改写为如下形式:
public class SimpleCacheDemo {
private Map<Integer, Future<Integer>> cache = new ConcurrentHashMap<>();
public Integer retrieve(Integer key) {
FutureTask<Integer> task = new FutureTask<>(() -> compute(key));
Future<Integer> result = cache.putIfAbsent(key, task);
if (result == null) {
result = task;
task.run();
}
try {
return result.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
private Integer compute(Integer value) {
// 模拟代价很高的计算
return value;
}
}
这个实现的缺陷在于,每次都要new一个FutureTask出来。可以作一个小优化,通过先get判断是否为空,如果为空再初始化一个FutrueTask用putIfAbsent扔到map中。
computeIfAbsent
实际上以上介绍的几种实现在《Java并发编程实战》中都有描述。
这本大师之作毕竟写作时还是JDK5和6的时代。在JDK8中,Map以及ConcurrentMap接口新增了computeIfAbsent的接口方法。在ConcurrentHashMap中的实现是具有原子语义的。所以实际上,上面的程序我们也可以不用FutureTask,直接用computeIfAbsent,代码如下:
public class SimpleCacheDemo {
private Map<Integer, Integer> cache = new ConcurrentHashMap<>();
public Integer retrieve(Integer key) {
return cache.computeIfAbsent(key, this::compute);
}
private Integer compute(Integer value) {
// 模拟代价很高的计算
return value;
}
}
总结
上面用简易的代码展示了在开发小型应用中时常需要的基于Map的简易缓存方案,考虑到的点在于线程安全、伸缩性以及避免重复计算等问题。如果代码还有其他地方有这样的需求,不妨抽象出一个小的框架出来。上面的代码中没有考虑到地方在于内存的使用消耗等,然而在实战中这是不能忽视的一点。
参考资料
- 《Java并发编程实战》
- 《Java并发编程的艺术》
--------------------------------------------------------------------P.S.一个知识点整理-------------------------------------------------------------
在最后一个例子中,用到了Map的computeIfAbsent()方法,
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
} return v;
}
这个方法是JDB1.8开始出现的。用了的Lambda表达式。
正如方法名锁揭示的那样,key值不存在的话,调用mappingFunction.apply(key)这个方法,进行key的相关处理,之后,会将key-value放进map里,最后返回新生成的value值。我们用Lambda表达式重写的也就是mappingFunction.apply(key)这个方法。
与之类似的computeIfPresent、compute都是8中新出现的方法,通过名字就可以理解,实现的代码上面的类似。
【转】基于Map的简易记忆化缓存的更多相关文章
- 基于Map的简易记忆化缓存
背景 在应用程序中,时常会碰到需要维护一个map,从中读取一些数据避免重复计算,如果还没有值则计算一下塞到map里的的小需求(没错,其实就是简易的缓存或者说实现记忆化).在公司项目里看到过有些代码中写 ...
- [HNOI2013]比赛 (用Hash实现记忆化搜索)
[HNOI2013]比赛 题目描述 沫沫非常喜欢看足球赛,但因为沉迷于射箭游戏,错过了最近的一次足球联赛.此次联 赛共N支球队参加,比赛规则如下: (1) 每两支球队之间踢一场比赛. (2) 若平局, ...
- HDU 1429 (BFS+记忆化状压搜索)
题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=1429 题目大意:最短时间内出迷宫,可以走回头路,迷宫内有不同的门,对应不同的钥匙. 解题思路: 要是 ...
- 【Hadoop学习】HDFS中的集中化缓存管理
Hadoop版本:2.6.0 本文系从官方文档翻译而来,转载请尊重译者的工作,注明以下链接: http://www.cnblogs.com/zhangningbo/p/4146398.html 概述 ...
- HDFS集中化缓存管理
概述 HDFS中的集中化缓存管理是一个明确的缓存机制,它允许用户指定要缓存的HDFS路径.NameNode会和保存着所需快数据的所有DataNode通信,并指导他们把块数据缓存在off-heap缓存中 ...
- HDU1978 记忆化搜索
How many ways Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Tot ...
- E. Santa Claus and Tangerines 二分答案 + 记忆化搜索
http://codeforces.com/contest/752/problem/E 首先有一个东西就是,如果我要检测5,那么14我们认为它能产生2个5. 14 = 7 + 7.但是按照平均分的话, ...
- hdu 4856 Tunnels (记忆化搜索)
Tunnels Time Limit: 3000/1500 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Total Su ...
- loj 1011(状态压缩+记忆化搜索)
题目链接:http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=25837 思路:状态压缩+记忆化搜索. #include<io ...
随机推荐
- mysql 数据迁移
最近线上系统新挂了一次磁盘,需要将系统磁盘下的 mysql 数据目录迁移到 数据盘上. 经过一番考察,mysql在安装时,使用了预编译的二进制tar.gz包.共有两处配置了 datadir属性 /et ...
- S2 深入.NET和C#编程 机试测试错题积累
机试第一题: //创建文件流(路径 模式) FileStream fa = new FileStream(@"D:\\S2第一次全文 ...
- 《Python》常用内置模块
一.time模块(时间模块) 三种格式: 1.时间戳时间(timestamp):浮点数,秒为单位,从1970年1月1日0时距今的时间 1970.1.1 0:0:0 英国伦敦时间(开始时间) 1970 ...
- 【JAVA多线程】interrupted() 和 isInterrupted() 的区别
Thread 类中提供了两种方法用来判断线程的状态是不是停止的.就是我们今天的两位主人公 interrupted() 和 isInterrupted() . interrupted() 官方解释:测试 ...
- eclipse server和tomcat的区别,将server的部署目录改到自己安装的tomcat中及如何设置tomcat用户
转:http://www.cnblogs.com/Yogurshine/archive/2013/06/05/3118525.html 一.发现问题(如果不把项目部署到tomcat的webapp目录下 ...
- loadrunner json参数化
因为json格式有{},所以LR参数化时如果也用默认的{}的话,会冲突,这样脚本运行时就无法识别,导致不能正确的读取参数化文件里的内容,此时把参数化的{}改成其他符号即可,比如<>
- CentOS 查看文件大小 du -hs filename
du -hs [filename] 查看目录大小 [root@localhost opt]# 16M apache-tomcat- df -hv 查看整个磁盘使用状况 [root@rabbit66 ...
- 第四周四则运算3 PSP表格
PSP2.1 Personal Software Process Stages time Plan 计划 -Estimate 整数四则运算 分数四则运算 括号 括号四则运算 在主函数中调用 30m ...
- L252 小组作业
Hans: Hi Good morning everyone! Let me introduce a new colleague for you, Berry Berry: Hi Good morni ...
- oracle 12c 警告日志位置
Oracle 12c环境下查询,alert日志并不在bdump目录下,看到网上和书上都写着可以通过初始化参数background_dump_dest来查看alter日志路径,还说警告日志文件的缺省位置 ...