使用HsahMap
首先我們定義一個Computable接口,該接口包含一個compute()方法,該方法是一個耗時很久的數值計算方法。Memoizer1是第一個版本的緩存,該版本使用hashMap來保存之前計算的結果,compute方法將首先檢查需要的結果是否已經在緩存中,如果存在則返回之前計算的值,否則重新計算並把結果緩存在HashMap中,然後再返回。
interface Computable<A, V> {
V compute(A arg) throws InterruptedException;//耗時計算
}
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;
}
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;
}
}
HashMap不是線程安全的,因此要確保兩個線程不會同時訪問HashMap,Memoizer1採用了一種保守的方法,即對整個方法進行同步。這種方法能確保線程安全性,但會帶來一個明顯的可伸縮問題:每次只有一個線程可以執行compute。
使用ConcurrentHashMap的版本
我們可以用ConcurrentHashMap代替HashMap來改進Memoizer1中糟糕的併發行爲,由於ConcurrentHashMap是線程安全的,因此在訪問底層Map時就不需要進行同步,因此避免了對compute()方法進行同步帶來的串行性:
public class Memoizer2 <A, V> implements Computable<A, V> {
private final Map<A, V> cache = new ConcurrentHashMap<A, V>();
private final Computable<A, V> c;
public Memoizer2(Computable<A, V> c) {
this.c = c;
}
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;
}
}
但是這個版本的緩存還是有問題的,如果線程A啓動了一個開銷很大的計算,而其他線程並不知道這個線程正在進行,那麼很可能會重複這個計算。
FutureTask版本1
我們可以在map中存放Future對象而不是最終計算結果,Future對象相當於一個佔位符,它告訴用戶,結果正在計算中,如果想得到最終結果,請調用get()方法。Future的get()方法是一個阻塞方法,如果結果正在計算中,那麼它會一直阻塞到結果計算完畢,然後返回;如果結果已經計算完畢,那麼就直接返回。
public class Memoizer3<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizer3(Computable<A, V> c) {
this.c = c;
}
public V compute(final A arg) throws InterruptedException {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = ft;
cache.put(arg, ft);
ft.run(); // call to c.compute happens here
}
try {
return f.get();
} catch (ExecutionException e) {
cache.remove(arg);
}
return null;
}
}
Memoizer3解決了上一個版本的問題,如果有其他線程在計算結果,那麼新到的線程會一直等待這個結果被計算出來,但是他又一個缺陷,那就是仍然存在兩個線程計算出相同值的漏洞。這是一個典型的”先檢查再執行”引起的競態條件錯誤,我們先檢查map中是否存在結果,如果不存在,那就計算新值,這並不是一個原子操作,所以兩個線程仍有可能在同一時間內調用compute來計算相同的值。
FutureTask版本2
Memoizer3存在這個問題的原因是,複合操作”若沒有則添加”不具有原子性,我們可以改用ConcurrentMap中的原子方法putIfAbsent,避免了Memoizer3的漏洞。
public class Memoizer <A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache
= new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizer(Computable<A, V> c) {
this.c = c;
}
public V compute(final A arg) throws InterruptedException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {
cache.remove(arg, f);
} catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
}
}
當緩存對象是Future而不是值的時候,將導致緩存污染問題,因爲某個計算可能被取消或失敗,當出現這種情況時,我們應該把對象從map中移除,然後再重新計算一遍。這個緩存系統的使用十分簡單,只需傳入一個Computable對象即可構造緩存,要得到計算結果,調用compute()方法即可,該方法會把計算過的結果緩存起來。