【转】基于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 ...
随机推荐
- 尚学堂java答案解析 第二章
本答案为本人个人编辑,仅供参考,如果读者发现,请私信本人或在下方评论,提醒本人修改 一.选择题: 1.CD 解析:A public是关键字. B 第一个不能是数字 2.C 解析:j=i++ < ...
- bzoj3930
题解: 莫比乌斯函数 然而向我这种弱菜肯定选择暴力dp 代码: #include<bits/stdc++.h> ,M=; typedef long long ll; using names ...
- flask-admin fileadmin 上传文件,中文名的解决方案 重写部分secure_filename
class upload_view(FileAdmin): def _save_form_files(self, directory, path, form): super() filename = ...
- useful links about machine learning
机器学习(Machine Learning)&深度学习(Deep Learning)资料(Chapter 1) 机器学习(Machine Learning)&深度学习(Deep Lea ...
- Myeclipse代码格式化
Shift+Ctrl+F 别和搜狗输入法冲突,把搜狗输入法的设置取消掉. 当然,还可以设置,自动格式化: 步骤:
- error MSB8008: 指定的平台工具集(V120)未安装或无效。
打开项目属性->配置属性->右面,平台工作集,选为v10 如下图
- Centos7部署kubernetes-ETCD集群(三)
1.下载etcd软件包 wget https://github.com/coreos/etcd/releases/download/v3.2.18/etcd-v3.2.18-linux-amd64.t ...
- CountDownLatch在多线程程序中的应用
一.CountDownLatch介绍 CountDownLatch是JDK1.5之后引入的,存在于java.util.concurrent包下,能够使一个线程等待其他线程完成动作后再执行.构造方法: ...
- 字典和json 的区别 和转换
前言:字典和json非常像.接下来比较一下两者的异同 先看一下字典的写法: a = {', ', ' } 再看一下json的写法: { "studentInfo": { " ...
- 关于orm 的基础3 day67
day67 ORM 特殊的语法 一个简单的语法 --翻译成--> SQL语句 语法: 1. 操作数据库表 创建表.删除表.修改表 2. 操作数据库行 增.删.改.查 怎么连数据库: 需要手动创建 ...