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

更多內容請訪問 flydean的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章