在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;
}
@Override
public 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;
}
@Override
public 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;
}
@Override
public V calculate(A arg) throws InterruptedException {
Future<V> future= cache.get(arg);
V result=null;
if( future ==null ){
Callable<V> callable= new Callable<V>() {
@Override
public 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;
}
@Override
public 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>() {
@Override
public 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

更多内容请访问 flydean的博客

在java中构建高效的结果缓存的更多相关文章

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

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

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

    原文出处: hollischuang(@Hollis_Chuang) 如何检查一个数组(无序)是否包含一个特定的值?这是一个在Java中经常用到的并且非常有用的操作.同时,这个问题在Stack Ove ...

  3. 在Java中怎样高效的推断数组中是否包括某个元素

    来自 http://www.hollischuang.com/archives/1269? 怎样检查一个数组(无序)是否包括一个特定的值?这是一个在Java中经经常使用到的并且非常实用的操作.同一时候 ...

  4. java中常用的几种缓存类型介绍

    在平时的开发中会经常用到缓存,比如locache.redis等,但一直没有对缓存有过比较全面的总结.下面从什么是缓存.为什么使用缓存.缓存的分类以及对每种缓存的使用分别进行分析,从而对缓存有更深入的了 ...

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

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

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

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

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

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

  8. java中如何高效的判断数组中是否包含某个元素---

    package zaLearnpackage; import org.apache.commons.lang3.ArrayUtils; import java.util.Arrays; import ...

  9. Java中如何使用Redis做缓存

    基本功能测试 1.程序基本结构 2.主要类 1)功能类 package com.redis; import java.util.ArrayList; import java.util.Iterator ...

随机推荐

  1. JAVA设计模式——(2)策略模式

    定义 定义一种算法,将每个算法都封装起来,并且使它们之间可以互换.是一种行为类模式. 举例 为了通俗易懂,我们拿各国的税率计算来举例子: 假设当前我们的程序只能支持计算中国和美国的税率: public ...

  2. 用命令在本地创建github仓库

    问题 每次创建github仓库,都要到github官网,有点麻烦,想在本地直接创建github仓库,写好项目后直接push. 操作系统:linux 步骤 1, 首先在github申请一个私人api t ...

  3. python基本知识点if、while、等等

    给予Python的相关知识点: 主要是对于基本语句的相关使用if else while for等先关的应用,以及步骤如下: 尝试编程如下:还有就是对于 import math import rando ...

  4. 路由与交换,cisco路由器配置,动态路由协议—RIP

    一.动态路由协议分类 动态路由协议包括IGP(内部网关协议)和EGP(外部网关协议). 1.IGP IGP又包括距离向量路由协议和链路状态路由协议. (1)距离向量路由协议典型代表:RIP (2)链路 ...

  5. 2017蓝桥杯购物单(C++B组)

    原题: 标题: 购物单 小明刚刚找到工作,老板人很好,只是老板夫人很爱购物.老板忙的时候经常让小明帮忙到商场代为购物.小明很厌烦,但又不好推辞.这不,XX大促销又来了!老板夫人开出了长长的购物单,都是 ...

  6. Java第十五天,泛型

    一.定义 泛型是一种未知的数据类型,即当我们不知道该使用哪种数据类型的时候,可以使用泛型. 泛型的本质是为了  参数化 类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型) ...

  7. idle中上传jar包并使用的方法

    创建一个lib目录,将jar包拉到该目录下. 需要导入的Jar包上,点击右键,选择Add as Library…

  8. 深入了解CI/CD:工具、方法、环境、基础架构的全面指南

    本文来自Rancher Labs 持续集成和持续交付(CI/CD)是DevOps背后的助推力之一.如果你的企业正在考虑使用DevOps,那么CI/CD绝对是需要考虑的其中一部分.但是CI/CD到底意味 ...

  9. JDK安装详细步骤

    JDK的下载与安装 在java实际编程的道路上,迈出的第一步必定是JDK的安装,因为JDK是一切java的基础,这里给出在Windows10 x64版本下的JDK1.8的详细安装步骤,其他的Windo ...

  10. AJ学IOS 之小知识iOS启动动画_Launch Screen的运用

    AJ 分享,必须精品 看下效果吧 例如新浪微博的软件开启时候 就是这个 用Launch image实现 这个不难,就是在Images.xcassets 增加一个LaunchImage文件(右键 new ...