前言
一臺計算機的核心是CPU,它是計算機系統的運算和控制核心。由於它處理運算速度快,所以基本都會給CPU配置一級快取,當CPU要讀取一個資料時,首先從快取中查詢,如果沒有在從記憶體或者磁碟塊中找。
同樣的,作為一個伺服器應用程式,為了讓應用程式執行更快速,響應更給力,我們會給它設定一些資料快取,這樣可以提高應用程式的吞吐量、縮短客戶端的響應時間。
建立快取過程分析
我們從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
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)方法獲取計算結果。