背景

在应用程序中,时常会碰到需要维护一个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. RowFilter遇上特殊字符*%'[]\

    开发时,时不时需要分析DataView的数据.计算,过滤等 .但是处理的值有可能会遇上是带有特殊字符*.%.'.[.]和\ .需要手动在表达式中,处理这些特殊字符,需要再使用"[" ...

  2. Java基础——Servlet(三)

    还在学习Servlet,觉得这里的知识点蛮多的.还要继续努力,加油. 拿韩老师的话激励一下自己,共勉.韩老师说,“成功其实也不难,只要树立一个目标,不需要你是一个很强的人,不需要你很高智商,不需要你是 ...

  3. Navicat相关应用及注意事项

    一.MySQL数据类型 1.数值型 SMALLINT: 2个字节 INT: 4个字节      // age int(10) INTEGER:INT的同义词 BIGINT : 8个字节 FLOAT : ...

  4. 创建一个jdbc连接

    本文介绍如何建立一个jdbc连接进行数据库查询操作. 创建一个java工程,导入jar包. 作者使用mysql数据库,建立jdbc连接需要mysql数据库驱动jar包和jdbc连接jar包. 建立jd ...

  5. Okhttp3 使用和原理(DEMO)

    基本使用: http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html http://www.jianshu.c ...

  6. Linux常用基本命令( tree, pwd, cd )

    pwd与cd命令 >pwd命令是“print working directory”中每个单词的首字母缩写,其功能是显示当前工作目录的绝对路径.在实际工作中,我们在命令行操作命令时,经常会在各个目 ...

  7. Codeforces445B(SummerTrainingDay06-N 并查集)

    B. DZY Loves Chemistry time limit per test:1 second memory limit per test:256 megabytes input:standa ...

  8. grunt搭建自动化的web前端开发环境(转)

    1. 前言 各位web前端开发人员,如果你现在还不知道grunt或者听说过.但是不会熟练使用grunt,那你就真的真的真的out了(三个“真的”重复,表示重点).至于grunt的作用,这里不详细说了, ...

  9. IIS 下调用证书出现异常解决方案 (C#)

    程序发布前,跑在vs上是没问题的,当发布后,程序就报错了.通过系统日志找到了错误所在:证书调用时出现了异常.原因是:在IIS上调用证书是需要配置的,具体配置如下: 一. 确保证书已安装 1. 点击 [ ...

  10. Android Studio 一个完整的APP实例(附源码和数据库)

    前言: 这是我独立做的第一个APP,是一个记账本APP. This is the first APP, I've ever done on my own. It's a accountbook APP ...