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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详细介绍高性能Java缓存库Caffeine

    1.介绍 在本文中,我们来看看Caffeine- 一个高性能的 Java 缓存库. 缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item. 回收策略为在指定时间删除哪些对象.此策略直接影响缓存的命中率 - 缓存库的一个重要特征. Caffeine 因使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率. 2.依赖 我们需要在 pom.xml 中添加 caffeine 依赖: <dependency> <groupId>com.github.ben-

  • 在Java中使用redisTemplate操作缓存的方法示例

    背景 在最近的项目中,有一个需求是对一个很大的数据库进行查询,数据量大概在几千万条.但同时对查询速度的要求也比较高. 这个数据库之前在没有使用Presto的情况下,使用的是Hive,使用Hive进行一个简单的查询,速度可能在几分钟.当然几分钟也并不完全是跑SQL的时间,这里面包含发请求,查询数据并且返回数据的时间的总和.但是即使这样,这样的速度明显不能满足交互式的查询需求. 我们的下一个解决方案就是Presto,在使用了Presto之后,查询速度降到了秒级.但是对于一个前端查询界面的交互式查询来

  • 如何基于LoadingCache实现Java本地缓存

    这篇文章主要介绍了如何基于LoadingCache实现Java本地缓存,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 Guava是Google开源出来的一套工具库.其中提供的cache模块非常方便,是一种与ConcurrentMap相似的缓存Map. 官方地址:https://github.com/google/guava/wiki/CachesExplained 开始构建 一. 添加依赖 <dependency> <groupI

  • 如何在 Java 中实现一个 redis 缓存服务

    缓存服务的意义 为什么要使用缓存?说到底是为了提高系统的运行速度.将用户频繁访问的内容存放在离用户最近,访问速度最快的地方,提高用户的响应速度.一个 web 应用的简单结构如下图. web 应用典型架构 在这个结构中,用户的请求通过用户层来到业务层,业务层在从数据层获取数据,返回给用户层.在用户量小,数据量不太大的情况下,这个系统运行得很顺畅.但是随着用户量越来越大,数据库中的数据越来越多,系统的用户响应速度就越来越慢.系统的瓶颈一般都在数据库访问上.这个时候可能会将上面的架构改成下面的来缓解数

  • Java缓存Map设置过期时间实现解析

    这篇文章主要介绍了Java缓存Map设置过期时间实现解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前言 最近项目需求需要一个类似于redis可以设置过期时间的K,V存储方式.项目前期暂时不引进redis,暂时用java内存代替. 解决方案 1. ExpiringMap 功能简介 : 1.可设置Map中的Entry在一段时间后自动过期. 2.可设置Map最大容纳值,当到达Maximum size后,再次插入值会导致Map中的第一个值过期.

  • Java内存缓存工具Guava LoadingCache使用解析

    这篇文章主要介绍了Java内存缓存工具Guava LoadingCache使用解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.Guava介绍 Guava是Google guava中的一个内存缓存模块,用于将数据缓存到JVM内存中.实际项目开发中经常将一些公共或者常用的数据缓存起来方便快速访问. Guava Cache是单个应用运行时的本地缓存.它不把数据存放到文件或外部服务器.如果不符合需求,可以选择Memcached.Redis等工具

  • JavaWeb禁止浏览器缓存当前Web页面的方法

    所谓浏览器缓存,是指当第一次访问网页时,浏览器会将这些网页缓存到本地,当下一次再访问这些被缓存的网页时,浏览器就会直接从本地读取这些网页的内容,而无需再从网络上获取. 虽然浏览器提供的缓存功能可以有效地提高网页的装载速度,但对于某些需要实时更新的网页,这种缓存机制就会影响网页的正常显示.幸好在HTTP响应消息头中提供了三个字段可以关闭客户端浏览器的缓存功能.下面三条语句分别使用这三个字段来关闭浏览器的缓存: response.setDateHeader("Expires", 0); r

  • Java构建高效结果缓存方法示例

    缓存是现代应用服务器中非常常用的组件.除了第三方缓存以外,我们通常也需要在java中构建内部使用的缓存.那么怎么才能构建一个高效的缓存呢? 本文将会一步步的进行揭秘. 使用HashMap 缓存通常的用法就是构建一个内存中使用的Map,在做一个长时间的操作比如计算之前,先在Map中查询一下计算的结果是否存在,如果不存在的话再执行计算操作. 我们定义了一个代表计算的接口: public interface Calculator<A, V> { V calculate(A arg) throws I

  • Vue自动构建发布脚本的方法示例

    简介 使用cross-env, scp2两个插件完成 cross-env cross-env这是一款运行跨平台设置和使用环境变量的脚本. 为什么需要cross-env? NODE_ENV=production 像这样设置环境变量时,大多数Windows命令提示符都会阻塞 .(Windows上的Bash是例外,它使用本机Bash.)同样,Windows和POSIX命令使用环境变量的方式也有所不同.对于POSIX,您可以使用: $ENV_VAR 和在Windows上可以使用 %ENV_VAR% .

  • Yii 2.0如何使用页面缓存方法示例

    前言 本文主要给大家介绍的是关于Yii2.0如何使用页面缓存的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍. 起初使用页面缓存,发现使用于含有参数的方法存在弊端,只能缓存第一次的页面,导致后面所有不同参数的页面均显示第一次缓存页面:没有生成一个参数页面一个缓存:于是,进行了重写页面缓存. 示例代码 <?php namespace common\lib; use Yii; use yii\caching\Cache; use yii\di\Instance; use yii\web\

  • java统计汉字字数的方法示例

    本文实例讲述了java统计汉字字数的方法.分享给大家供大家参考,具体如下: public class TongJiHanZi { public static int count(String text) { String Reg="^[\u4e00-\u9fa5]{1}$";//正则 int result=0; for(int i=0;i<text.length();i++){ String b=Character.toString(text.charAt(i)); if(b.m

  • Java计算黑洞数的方法示例

    本文实例讲述了Java计算黑洞数的方法.分享给大家供大家参考,具体如下: 任意一个5位数,比如:34256,把它的各位数字打乱,重新排列,可以得到一个最大的数:65432,一个最小的数23456.求这两个数字的差,得:41976,把这个数字再次重复上述过程(如果不足5位,则前边补0).如此往复,数字会落入某个循环圈(称为数字黑洞). 比如,刚才的数字会落入:[82962,75933, 63954, 61974]这个循环圈. 请编写程序,找到5位数所有可能的循环圈,并输出,每个循环圈占1行.其中5

  • java实现合并图片的方法示例

    本文实例讲述了java实现合并图片的方法.分享给大家供大家参考,具体如下: package com.test; import java.io.File; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; public class ImageCombineTest { public static void main(String args[]) { try { // 读取第一张图片 File fileOne = n

  • Python构建XML树结构的方法示例

    本文实例讲述了Python构建XML树结构的方法.分享给大家供大家参考,具体如下: 1.构建XML元素 #encoding=utf-8 from xml.etree import ElementTree as ET import sys root=ET.Element('color') #用Element类构建标签 root.text=('black') #设置元素内容 tree=ET.ElementTree(root) #创建数对象,参数为根节点对象 tree.write(sys.stdout

  • Java构建乘积数组的方法

    本文实例为大家分享了Java构建乘积数组的具体实现代码,供大家参考,具体内容如下 给定一个数组A[0,1,-,n-1],请构建一个数组B[0,1,-,n-1],其中B中的元素B[i]=A[0]A[1]-A[i-1]*A[i+1]-*A[n-1]. 不能使用除法. 代码 解法一 暴力法,这是本能就能想到的解决办法. public static int[] multiply(int[] array) { if (array == null) { return null; } int len = ar

  • Vue的缓存方法示例详解

    最近新做了个需求"前端缓存" 需求背景:解决表单高频率重复填报问题,要求打开页面自动填充上次录入的数据,数据存储期限为一周(7天有效期). 说起缓存首先想到的则是 localstorage.sessionStorage sessionStorage 也称会话缓存,当用户关闭浏览器窗口后,数据就会被删除: sessionStorage.setItem("key","value");//存储 sessionStorage.getItems("

  • Java实现高效随机数算法的示例代码

    前言 事情起源于一位网友分享了一个有趣的面试题: 生成由六位数字组成的ID,要求随机数字,不排重,不可自增,且数字不重复.ID总数为几十万. 初次解答 我一开始想到的办法是 生成一个足够大的ID池(其实就是需要多少就生成多少) 对ID池中的数字进行随机排序 依次消费ID池中的数字 可惜这个方法十分浪费空间,且性能很差. 初遇梅森旋转算法 后面咨询了网友后得知了一个高效的随机数算法:梅森旋转(Mersenne Twister/MT).通过搜索资料得知: 梅森旋转算法(Mersenne twiste

随机推荐