背景

在应用程序中,时常会碰到需要维护一个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并发编程的艺术》

基于Map的简易记忆化缓存的更多相关文章

  1. 【转】基于Map的简易记忆化缓存

    看到文章后,自己也想写一些关于这个方面的,但是觉得写的估计没有那位博主好,而且又会用到里面的许多东西,所以干脆转载.但是会在文章末尾写上自己的学习的的东西. 原文出处如下: http://www.cn ...

  2. [HNOI2013]比赛 (用Hash实现记忆化搜索)

    [HNOI2013]比赛 题目描述 沫沫非常喜欢看足球赛,但因为沉迷于射箭游戏,错过了最近的一次足球联赛.此次联 赛共N支球队参加,比赛规则如下: (1) 每两支球队之间踢一场比赛. (2) 若平局, ...

  3. HDU 1429 (BFS+记忆化状压搜索)

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=1429 题目大意:最短时间内出迷宫,可以走回头路,迷宫内有不同的门,对应不同的钥匙. 解题思路: 要是 ...

  4. 【Hadoop学习】HDFS中的集中化缓存管理

    Hadoop版本:2.6.0 本文系从官方文档翻译而来,转载请尊重译者的工作,注明以下链接: http://www.cnblogs.com/zhangningbo/p/4146398.html 概述 ...

  5. HDFS集中化缓存管理

    概述 HDFS中的集中化缓存管理是一个明确的缓存机制,它允许用户指定要缓存的HDFS路径.NameNode会和保存着所需快数据的所有DataNode通信,并指导他们把块数据缓存在off-heap缓存中 ...

  6. HDU1978 记忆化搜索

    How many ways Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Tot ...

  7. E. Santa Claus and Tangerines 二分答案 + 记忆化搜索

    http://codeforces.com/contest/752/problem/E 首先有一个东西就是,如果我要检测5,那么14我们认为它能产生2个5. 14 = 7 + 7.但是按照平均分的话, ...

  8. hdu 4856 Tunnels (记忆化搜索)

    Tunnels Time Limit: 3000/1500 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Su ...

  9. loj 1011(状态压缩+记忆化搜索)

    题目链接:http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=25837 思路:状态压缩+记忆化搜索. #include<io ...

随机推荐

  1. JS存储cookie读取cookie删除cookie详细用法

    假设有这样一种情况,在某个用例流程中,由A页面跳至B页面,若在A页面中采用JS用变量temp保存了某一变量的值,在B页面的时候,同样需要使用JS来引用temp的变量值,对于JS中的全局变量或者静态变量 ...

  2. C++ 小知识点 WINAPI

    int WINAPI WINMain 中,WINAPI含义 网友给出回答:在windef.h头文件中有如下定义#define WINAPI      __stdcall#define APIENTRY ...

  3. [日常] Go语言圣经-并发的非阻塞缓存

    1.go test命令是一个按照约定和组织进行测试的程序2.竞争检查器 go run -race 附带一个运行期对共享变量访问工具的test,出现WARNING: DATA RACE 说明有数据竞争3 ...

  4. C#进行数据筛选(一)

    这里介绍数据筛选的第一种方式,不用过滤器,给新手看得 public DataTable SourceList(string Wmain, string OrderNo, string Process) ...

  5. POJ3186(KB12-O DP)

    Treats for the Cows Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 5801   Accepted: 30 ...

  6. Angular echarts图表自适应屏幕指令

    关于echarts图表自适应问题 一.引入js文件 1. 在html页面引入angular.min.js文件 2. 在html页面引入echarts.min.js文件 3. 在html页面引入app. ...

  7. MySql概念及常用Sql

    MySQL是一个关系型数据库管理系统 MySQL启动命令: 本机mysql地址D:\pefession\mySql\mysql-8.0.12-winx64\bin 启动服务:net start mys ...

  8. 利用 js-xlsx 实现 Excel 文件导入并解析Excel数据成json格式的数据并且获取其中某列数据

    演示效果参考如下:XML转JSON 另一个搭配SQL实现:http://sheetjs.com/sexql/index.html 详细介绍: 1.首先需要导入js <script src=&qu ...

  9. nodejs 通过nginx后出现响应慢的解决方法

    最近用了nodejs搭建服务器,然后用了nginx做了反向代理,项目开发需求,没办法.但是发现了经过代理之后发现网页请求变慢了,而且是不能忍的一分钟以上. 一开始,怀疑是在nodejs那边的问题,结果 ...

  10. Leaflet.draw 无法编辑multipolygon类型多边形 解决方法

    问题说明 在做面要素的编辑的时候,当对multiploygon类型的面要素进行编辑的时候,出现如下错误: TypeError: Cannot read property 'lat' of null 通 ...