一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Java如何构建高效结果缓存 Java构建高效结果缓存方法

时间:2020-04-24 编辑:袖梨 来源:一聚教程网

Java如何构建高效结果缓存?本篇文章小编给大家分享一下Java构建高效结果缓存方法,文章代码介绍的很详细,小编觉得挺不错的,现在分享给大家供大家参考,有需要的小伙伴们可以来看看。

使用HashMap

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

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

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

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

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

我们看下用HashMap怎么实现:

public class MemoizedCalculator1 implements Calculator {

  private final Map cache= new HashMap();
  private final Calculator calculator;
  public MemoizedCalculator1(Calculator 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 implements Calculator {

  private final Map cache= new ConcurrentHashMap<>();
  private final Calculator calculator;
  public MemoizedCalculator2(Calculator 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 implements Calculator {

  private final Map> cache= new ConcurrentHashMap<>();
  private final Calculator calculator;

  public MemoizedCalculator3(Calculator calculator){
    this.calculator=calculator;
  }
  @Override
  public V calculate(A arg) throws InterruptedException {
    Future future= cache.get(arg);
    V result=null;
    if( future ==null ){
      Callable callable= new Callable() {
        @Override
        public V call() throws Exception {
          return calculator.calculate(arg);
        }
      };
      FutureTask 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 implements Calculator {

  private final Map> cache= new ConcurrentHashMap<>();
  private final Calculator calculator;

  public MemoizedCalculator4(Calculator calculator){
    this.calculator=calculator;
  }
  @Override
  public V calculate(A arg) throws InterruptedException {
    while (true) {
      Future future = cache.get(arg);
      V result = null;
      if (future == null) {
        Callable callable = new Callable() {
          @Override
          public V call() throws Exception {
            return calculator.calculate(arg);
          }
        };
        FutureTask 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移除。

热门栏目