当前位置: 移动技术网 > IT编程>开发语言>Java > Java构建高效结果缓存方法示例

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

2020年05月12日  | 移动技术网IT编程  | 我要评论

中国民生在线,辛集,曝女镇长与上司车震被派出所长丈夫捉奸

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

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网