1. 程式人生 > 程式設計 >Java構建高效結果快取方法示例

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> 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,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> 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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。