文章目录

  • 使用HashMap
  • 使用ConcurrentHashMap
  • FutureTask

在java中构建高效的结果缓存

缓存是现代应用服务器中非常常用的组件。除了第三方缓存以外,我们通常也需要在java中构建内部使用的缓存。那么怎么才能构建一个高效的缓存呢? 本文将会一步步的进行揭秘。

使用HashMap

缓存通常的用法就是构建一个内存中使用的Map,在做一个长时间的操作比如计算之前,先在Map中查询一下计算的结果是否存在,如果不存在的话再执行计算操作。

我们定义了一个代表计算的接口:

public interface Calculator<A, V> {V calculate(A arg) throws InterruptedException;
}

该接口定义了一个calculate方法,接收一个参数,并且返回计算的结果。

我们要定义的缓存就是这个Calculator具体实现的一个封装。

我们看下用HashMap怎么实现:

public class MemoizedCalculator1<A, V> implements Calculator<A, V> {private final Map<A, V> cache= new HashMap<A, V>();private final Calculator<A, V> calculator;public MemoizedCalculator1(Calculator<A, V> calculator){this.calculator=calculator;}@Overridepublic synchronized V calculate(A arg) throws InterruptedException {V result= cache.get(arg);if( result ==null ){result= calculator.calculate(arg);cache.put(arg, result);}return result;}
}

MemoizedCalculator1封装了Calculator,在调用calculate方法中,实际上调用了封装的Calculator的calculate方法。

因为HashMap不是线程安全的,所以这里我们使用了synchronized关键字,从而保证一次只有一个线程能够访问calculate方法。

虽然这样的设计能够保证程序的正确执行,但是每次只允许一个线程执行calculate操作,其他调用calculate方法的线程将会被阻塞,在多线程的执行环境中这会严重影响速度。从而导致使用缓存可能比不使用缓存需要的时间更长。

使用ConcurrentHashMap

因为HashMap不是线程安全的,那么我们可以尝试使用线程安全的ConcurrentHashMap来替代HashMap。如下所示:

public class MemoizedCalculator2<A, V> implements Calculator<A, V> {private final Map<A, V> cache= new ConcurrentHashMap<>();private final Calculator<A, V> calculator;public MemoizedCalculator2(Calculator<A, V> calculator){this.calculator=calculator;}@Overridepublic V calculate(A arg) throws InterruptedException {V result= cache.get(arg);if( result ==null ){result= calculator.calculate(arg);cache.put(arg, result);}return result;}
}

上面的例子中虽然解决了之前的线程等待的问题,但是当有两个线程同时在进行同一个计算的时候,仍然不能保证缓存重用,这时候两个线程都会分别调用计算方法,从而导致重复计算。

我们希望的是如果一个线程正在做计算,其他的线程只需要等待这个线程的执行结果即可。很自然的,我们想到了之前讲到的FutureTask。FutureTask表示一个计算过程,我们可以通过调用FutureTask的get方法来获取执行的结果,如果该执行正在进行中,则会等待。

下面我们使用FutureTask来进行改写。

FutureTask

@Slf4j
public class MemoizedCalculator3<A, V> implements Calculator<A, V> {private final Map<A, Future<V>> cache= new ConcurrentHashMap<>();private final Calculator<A, V> calculator;public MemoizedCalculator3(Calculator<A, V> calculator){this.calculator=calculator;}@Overridepublic V calculate(A arg) throws InterruptedException {Future<V> future= cache.get(arg);V result=null;if( future ==null ){Callable<V> callable= new Callable<V>() {@Overridepublic V call() throws Exception {return calculator.calculate(arg);}};FutureTask<V> futureTask= new FutureTask<>(callable);future= futureTask;cache.put(arg, futureTask);futureTask.run();}try {result= future.get();} catch (ExecutionException e) {log.error(e.getMessage(),e);}return result;}
}

上面的例子,我们用FutureTask来封装计算,并且将FutureTask作为Map的value。

上面的例子已经体现了很好的并发性能。但是因为if语句是非原子性的,所以对这一种先检查后执行的操作,仍然可能存在同一时间调用的情况。

这个时候,我们可以借助于ConcurrentHashMap的原子性操作putIfAbsent来重写上面的类:

@Slf4j
public class MemoizedCalculator4<A, V> implements Calculator<A, V> {private final Map<A, Future<V>> cache= new ConcurrentHashMap<>();private final Calculator<A, V> calculator;public MemoizedCalculator4(Calculator<A, V> calculator){this.calculator=calculator;}@Overridepublic V calculate(A arg) throws InterruptedException {while (true) {Future<V> future = cache.get(arg);V result = null;if (future == null) {Callable<V> callable = new Callable<V>() {@Overridepublic V call() throws Exception {return calculator.calculate(arg);}};FutureTask<V> futureTask = new FutureTask<>(callable);future = cache.putIfAbsent(arg, futureTask);if (future == null) {future = futureTask;futureTask.run();}try {result = future.get();} catch (CancellationException e) {log.error(e.getMessage(), e);cache.remove(arg, future);} catch (ExecutionException e) {log.error(e.getMessage(), e);}return result;}}}
}

上面使用了一个while循环,来判断从cache中获取的值是否存在,如果不存在则调用计算方法。

上面我们还要考虑一个缓存污染的问题,因为我们修改了缓存的结果,如果在计算的时候,计算被取消或者失败,我们需要从缓存中将FutureTask移除。

本文的例子可以参考https://github.com/ddean2009/learn-java-concurrency/tree/master/MemoizedCalculate

更多精彩内容且看:

  • 区块链从入门到放弃系列教程-涵盖密码学,超级账本,以太坊,Libra,比特币等持续更新
  • Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新
  • Spring 5.X系列教程:满足你对Spring5的一切想象-持续更新
  • java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程

更多内容请访问 flydean的博客

在java中构建高效的结果缓存相关推荐

  1. 如何使用布隆过滤器在Java中建立大容量的内存缓存

    背景 缓存是解决日常软件问题的重要概念. 您的应用程序可能会执行CPU密集型操作,而您又不想一次又一次地执行这些操作,而是只导出一次结果并将其缓存在内存中. 有时瓶颈是IO,例如您不想重复访问数据库, ...

  2. 在Java中如何高效的判断数组中是否包含某个元素

    如何检查一个数组(无序)是否包含一个特定的值?这是一个在Java中经常用到的并且非常有用的操作.同时,这个问题在Stack Overflow中也是一个非常热门的问题.在投票比较高的几个答案中给出了几种 ...

  3. go 判断元素是否在slice_在Java中如何高效判断数组中是否包含某个元素

    如何检查一个数组(无序)是否包含一个特定的值?这是一个在Java中经常用到的并且非常有用的操作.同时,这个问题在Stack Overflow中也是一个非常热门的问题.在投票比较高的几个答案中给出了几种 ...

  4. Java中如何高效的拼接字符串

    目录 写在前面 常规的字符串拼接方法 写在前面 这是一篇非常基础的文章,将会演示如何使用Java正确高效的拼接字符串. 这些问题也是我们应该注意的基础的性能优化技巧. 常规的字符串拼接方法 使用'+' ...

  5. java并发——构建高效且可伸缩的结果缓存

    几乎所有的服务器应用都会使用某种形式的缓存.重用之前的计算结果能降低延迟,提高吞吐量,但却要消耗更多内存.看上去简单的缓存,可能会将性能瓶颈转变成伸缩性瓶颈,即使缓存是用来提高单线程性能的.本文将开发 ...

  6. 使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制

    为了提高性能,几乎所有互联网应用都有缓存机制,其中Memcache是使用非常广泛的一个分布式缓存系统.众所周知,LAMP是非常经典的Web架构方式,但是随着Nginx的成熟,越来越多的系统开始转型为L ...

  7. [转] 使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制

    为了提高性能,几乎所有互联网应用都有缓存机制,其中Memcache是使用非常广泛的一个分布式缓存系统.众所周知,LAMP是非常经典的Web架构方式,但是随着Nginx的 成熟,越来越多的系统开始转型为 ...

  8. 转:使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制

    原文地址:http://blog.codinglabs.org/articles/nginx-memc-and-srcache.html 为了提高性能,几乎所有互联网应用都有缓存机制,其中Memcac ...

  9. 在Java中构建响应式微服务系统——第三章 构建响应式微服务

    第三章 构建响应式微服务 在本章中,我们将使用Vert.x构建我们的第一个微服务.由于大多数微服务系统使用HTTP进行交互,因此我们将以HTTP微服务作为开始.但是由于系统包含多个相互通讯的微服务,因 ...

最新文章

  1. python的init有什么用_Python中 __init__.py的作用
  2. BizTalk动手实验(十六)EDI-AS2解决文案开发配置
  3. xp怎样安装android-studio,Xposed 框架的安装
  4. 微信小程序云开发校园社交二手物品跳蚤平台表白动态求助寻物组队,完整免费,配置即用
  5. 数据规则列表加导入导出
  6. update fabric from 1.3 to 1.8
  7. Python paho-mqtt 模块使用(转)
  8. 合格前端系列第九弹-前端面试那些事
  9. 双目测距(一)--图像获取与单目标定
  10. thoughtworks业务需求分析师面试总结
  11. 人这一辈子,都在为选择买单
  12. linux配置网口的ip地址,Linux基本操作和基础命令(Linux修改IP地址以及修改网卡地址)...
  13. 计算机无法上无线网络连接到internet,电脑连接不上无线网络,教您怎么解决电脑连接不上无线网络...
  14. 联机棋类游戏《憋尿罐》实现源码
  15. 腾讯会议突围背后:端到端实时语音技术是如何保障交流通畅的?
  16. 【低功耗蓝牙BLE】连接事件和相关参数
  17. 关于bili处理视频文件遇到问题记录
  18. [亲测可行]Ubuntu16.04+opencv3.4+opencv_contrib+cuda9.0安装
  19. MFC 关于OnPaint绘图的一些经验
  20. SQL SERVER数据库迁移操作

热门文章

  1. ATC计算机会议,三项成果被计算机系统重要国际会议USENIX ATC和HotStorage收录
  2. linux wine运行效率,Wine 3.0让Windows应用在Linux上流畅运行!
  3. php serialize mysql_php 序列化(serialize)格式详解
  4. 浪潮服务器更换硬盘_总金额2.5亿!浪潮信息助力中国移动部署NFV项目
  5. 关于ax+by+cz的最大不可表数
  6. 用SSDT方法恢复冒险岛的部分函数
  7. MySQL(五)MySQL事务
  8. 为什么将0.1f改为0会使性能降低10倍?
  9. 干货!全面认识Docker和基本指令
  10. shell编程之case语句及函数