創建高速緩存機制-java版

前言

​ 一臺計算機的核心是CPU,它是計算機系統的運算和控制核心。因爲它處理運算速度快,因此基本都會給CPU配置一級緩存,當CPU要讀取一個數據時,首先從緩存中查詢,若是沒有在從內存或者磁盤塊中找。
​ 一樣的,做爲一個服務器應用程序,爲了讓應用程序運行更快速,響應更給力,咱們會給它設置一些數據緩存,這樣能夠提升應用程序的吞吐量、縮短客戶端的響應時間。java

創建緩存過程分析

​ 咱們從java最經常使用的方案開始——一個簡單的HashMap。緩存

public interface Computable<A, V> {
    V compute(A arg) throws InterruptedException;
}
public class ExpensiveFunction implements Computable<String, BigInteger> {
    @Override
    public BigInteger compute(String arg) throws InterruptedException {
        // after deep thought...
        return new BigInteger(arg);
    }
}

​ Computable<A, V>接口描述了一個功能,輸入類型是A,輸出結果的類型是V。ExpensiveFunction實現了Computable。須要花比較長的時間來計算結果。因此咱們計劃把計算過的值都放進一個HashMap中,這樣下一次有同一個A值進來時,直接獲取A的計算結果。安全

2.1 Synchronized版

public class Memoizer1<A, V> implements Computable<A, V> {
    private final Map<A, V> cache = new HashMap<A, V>();
    private final Computable<A, V> c;
    public Memoizer1(Computable<A, V> c) {
        this.c = c;
    }
    @Override
    public synchronized V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

​ Memoizer1實現了第一個版本,HashMap不是線程安全的,因此使用synchronzied關鍵字來保證線程安全,若是cache變量中有計算結果,直接從cache取,不須要再次計算,省下許多時間。但使用synchronzied使得一次只有一個線程可以執行compute。若是一個線程正在計算結果,那其餘調用compute的線程可能被阻塞很長時間,形成性能降低,這不是咱們但願經過緩存獲得的性能優化結果。性能優化

2.2 ConcurrentHashMap版

public class Memoizer2<A, V> implements Computable<A, V> {
    private final Map<A, V> cache = new ConcurrentHashMap<>();
    private final Computable<A, V> c;
    public Memoizer2(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

​ Memoizer2用ConcurrentHashMap取代HashMap,改進了Memoizer1中那種糟糕的併發行爲。由於ConcurrentHashMap是線程安全的,因此不須要使用Synchronized關鍵字,而是使用內部hash桶的分段鎖機制。
​ Memoizer2與Memoizer1相比,毫無疑問具備了更好的併發性:多線程能夠真正併發訪問了。可是做爲高速緩存仍然存在缺陷:當兩個線程同時調用compute時,若是是計算同一個值,此時compute是須要很大的開銷的,在一個線程還在計算中時,其它線程不知道,因此可能會重複計算。而咱們但願的是:若是A線程正在計算arg,那B就不要重複計算arg,等A計算好,直接取arg對應的V就行了。服務器

2.3 ConcurrentHashMap + FutureTask版

public class Memoizer3<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
    private final Computable<A, V> c;
    public Memoizer3(Computable<A, V> c) {
        this.c = c;
    }
    @Override
    public V compute(A arg) throws InterruptedException {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = () -> {
                return c.compute(arg);
            };
            FutureTask<V> ft = new FutureTask<>(eval);
            f = ft;
            cache.put(arg, ft);
            ft.run(); // 調用 c.compute發生在這裏
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

​ Memoizer3爲緩存的值從新定義可存儲Map,用ConcurrentHashMap<A, Future >取代ConcurrentHashMap<A,V>。Memoizer3首先檢查一個相應的計算是否開始,若是不是,就建立一個FutureTash,並把它註冊到map中,並開始計算,若是是,那麼它就會等待正在計算的結果。
​ Memoizer3的實現近乎是完美的:它展現了很是好的併發性,能很快返回已經計算過的結果,若是新到的線程請求的是其它線程正在計算的結果,它也會耐心的等待。
​ Memoizer3只有一個問題,就是仍然存在這種可能性:2個線程同時計算arg,此時因爲compute中的if代碼塊是非原子性的複合操做,2個線程會同時進入到if代碼塊中,依舊會同時計算同一個arg。但ConcurrentHashMap中提供了一個原子化的putIfAbsent方法,能夠消除Memoizer3的隱患。
多線程

2.4 最終版

public class Memoizer<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
    private final Computable<A, V> c;
    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws InterruptedException {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = () -> {
                return c.compute(arg);
            };
            FutureTask<V> ft = new FutureTask<>(eval);
            f = ft;
            cache.putIfAbsent(arg, ft);
            ft.run(); // 調用 c.compute發生在這裏
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

​ Memoizer能夠說是緩存的完美實現了:支持高併發,同一個參數計算也不會重複執行(多虧於ConcurrentHashMap的putIfAbsent原子化操做)。最終調用者經過調用Future.get(arg)方法獲取計算結果。併發

相關文章
相關標籤/搜索