欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-07-04 18:36:49
缓存是现代应用服务器中非常常用的组件。除了第三方缓存以外,我们通常也需要在java中构建内部使用的缓存。那么怎么才能构建一个高效的缓存呢? 本文将会一步步的进行揭秘。使用hashmap缓存通常的用法就...

缓存是现代应用服务器中非常常用的组件。除了第三方缓存以外,我们通常也需要在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

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