在這篇博客中,主要把之前看的書的內容記錄一下,個人感覺還是可以的,原題是這樣的:開發一個高效的緩存。這里指的是單機.
首先我來看當前的一個版本
1 public interface Computable<T, R> { 2 R compute(T input) throws InterruptedException; 3 }
1 public class Memoizer1<T,R> implements Computable<T,R>{ 2 3 private final Map<T,R> cache = new HashMap<>(); 4 5 private final Computable<T,R> computable; 6 7 public Memoizer1(Computable<T, R> computable) { 8 this.computable = computable; 9 } 10 11 public synchronized R compute(T input) throws InterruptedException { 12 R result= cache.get(input); 13 if(result ==null){ 14 result = computable.compute(input); 15 cache.put(input,result); 16 } 17 return result; 18 } 19 }
在該版本中利用HashMap來保存之前計算的結果,compute方法首先檢查緩存中是否有結果,沒有則計算,把其結果放入緩存並且返回。大家都知道HashMap不是線程安全的,因此要確保多個線程同時訪問的時,Memoizer1采用把對整個方法compute進行同步,這樣的結果導致調用該方法被串行化,如果compute的執行時間比較長,那么后面的線程需要等待更長的時間,結果可能比不用緩存更加糟糕。
接着我們對該版本進行進一步的優化,把HashMap改為ConcurrentHashMap,因為ConcurrentHashMao是線程安全的,因此在訪問底層的Map的時候不需要進行同步。因此避免了在對compute方法進行同步帶來的串行性。
1 public class Memoizer2<T,R> implements Computable<T,R>{ 2 3 private final Map<T,R> cache = new ConcurrentHashMap<>(); 4 5 private final Computable<T,R> computable; 6 7 public Memoizer2(Computable<T, R> computable) { 8 this.computable = computable; 9 } 10 11 public R compute(T input) throws InterruptedException { 12 R result= cache.get(input); 13 if(result ==null){ 14 result = computable.compute(input); 15 cache.put(input,result); 16 } 17 return result; 18 } 19 }
在該版本也存在一些不足,當兩個線程同時調用compute時存在一個漏洞,可能會導致計算相同的值。緩存的目的是避免相同的數據被多次計算。我們知道FutureTask 表示一個計算過程,這個過程可能已經完成,也可能正在進行。如果FutureTask結果可用,調用FutureTask.get()將立即得到結果,否則它會一直阻塞,知道計算結果出來再返回。
1 public class Memoizer3<T,R> implements Computable<T,R>{ 2 3 private final Map<T,Future> cache = new ConcurrentHashMap<>(); 4 5 private final Computable<T,R> computable; 6 7 public Memoizer3(Computable<T, R> computable) { 8 this.computable = computable; 9 } 10 11 public R compute(final T input) throws InterruptedException { 12 Future<R> future = cache.get(input); 13 if (future==null){ 14 Callable<R> callable = new Callable<R>() { 15 @Override 16 public R call() throws Exception { 17 return computable.compute(input); 18 } 19 }; 20 FutureTask futureTask = new FutureTask(callable); 21 future=futureTask; 22 cache.put(input,future); 23 futureTask.run(); 24 } 25 try{ 26 return future.get(); 27 } catch (ExecutionException e) { 28 throw new RuntimeException(e); 29 } 30 } 31 }
在第三個版本Memoizer3的實現幾乎是完美的,它表現出非常好的並發性,如果結果已經計算出來則直接返回,如果其他線程正在計算該結果,那么新到的線程將一直等待這個結果被計算出來。它只有一個缺陷,即仍然存在兩個線程計算出相同值的漏洞。
但是這個概率將遠遠小於第二個版本。由於compute方法中的if代碼仍然是非原子的“先檢查后執行”操作。因此兩個線程仍然有可能在同一時間內調用compute來計算相同的值。在該版本中存在該問題的原因是,復合操作在底層Map對象上執行,而這個對象無法通過加鎖來確保原子性。那么接着把Map.put()改為Map.putIfAbsent()即可。
1 public class Memoizer4<T,R> implements Computable<T,R>{ 2 3 private final ConcurrentMap<T,Future> cache = new ConcurrentHashMap<>(); 4 5 private final Computable<T,R> computable; 6 7 public Memoizer4(Computable<T, R> computable) { 8 this.computable = computable; 9 } 10 11 public R compute(final T input) throws InterruptedException { 12 while (true){ 13 Future<R> future = cache.get(input); 14 if (future==null){ 15 Callable<R> callable = new Callable<R>() { 16 @Override 17 public R call() throws InterruptedException { 18 return computable.compute(input); 19 } 20 }; 21 FutureTask futureTask = new FutureTask(callable); 22 future= cache.putIfAbsent(input, futureTask);//如果原先不存在,返回null 23 if(future==null){ 24 future=futureTask; 25 futureTask.run(); 26 } 27 } 28 try{ 29 return future.get(); 30 } catch (CancellationException e) { 31 cache.remove(input,future); 32 } catch (ExecutionException e) { 33 throw new RuntimeException(e); 34 } 35 } 36 }
在該版本中應該完美了解決了原先提出的問題。但是還是存在如下問題:沒有解決緩存預期的問題,沒有解決緩存清理問題。
