Java構(gòu)建高效結(jié)果緩存方法示例
緩存是現(xiàn)代應(yīng)用服務(wù)器中非常常用的組件。除了第三方緩存以外,我們通常也需要在java中構(gòu)建內(nèi)部使用的緩存。那么怎么才能構(gòu)建一個高效的緩存呢? 本文將會一步步的進行揭秘。
使用HashMap
緩存通常的用法就是構(gòu)建一個內(nèi)存中使用的Map,在做一個長時間的操作比如計算之前,先在Map中查詢一下計算的結(jié)果是否存在,如果不存在的話再執(zhí)行計算操作。
我們定義了一個代表計算的接口:
public interface Calculator<A, V> { V calculate(A arg) throws InterruptedException;}
該接口定義了一個calculate方法,接收一個參數(shù),并且返回計算的結(jié)果。
我們要定義的緩存就是這個Calculator具體實現(xiàn)的一個封裝。
我們看下用HashMap怎么實現(xiàn):
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,在調(diào)用calculate方法中,實際上調(diào)用了封裝的Calculator的calculate方法。
因為HashMap不是線程安全的,所以這里我們使用了synchronized關(guān)鍵字,從而保證一次只有一個線程能夠訪問calculate方法。
雖然這樣的設(shè)計能夠保證程序的正確執(zhí)行,但是每次只允許一個線程執(zhí)行calculate操作,其他調(diào)用calculate方法的線程將會被阻塞,在多線程的執(zhí)行環(huán)境中這會嚴重影響速度。從而導(dǎo)致使用緩存可能比不使用緩存需要的時間更長。
使用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; }}
上面的例子中雖然解決了之前的線程等待的問題,但是當(dāng)有兩個線程同時在進行同一個計算的時候,仍然不能保證緩存重用,這時候兩個線程都會分別調(diào)用計算方法,從而導(dǎo)致重復(fù)計算。
我們希望的是如果一個線程正在做計算,其他的線程只需要等待這個線程的執(zhí)行結(jié)果即可。很自然的,我們想到了之前講到的FutureTask。FutureTask表示一個計算過程,我們可以通過調(diào)用FutureTask的get方法來獲取執(zhí)行的結(jié)果,如果該執(zhí)行正在進行中,則會等待。
下面我們使用FutureTask來進行改寫。
FutureTask
@Slf4jpublic 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>() {@Overridepublic 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。
上面的例子已經(jīng)體現(xiàn)了很好的并發(fā)性能。但是因為if語句是非原子性的,所以對這一種先檢查后執(zhí)行的操作,仍然可能存在同一時間調(diào)用的情況。
這個時候,我們可以借助于ConcurrentHashMap的原子性操作putIfAbsent來重寫上面的類:
@Slf4jpublic 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循環(huán),來判斷從cache中獲取的值是否存在,如果不存在則調(diào)用計算方法。
上面我們還要考慮一個緩存污染的問題,因為我們修改了緩存的結(jié)果,如果在計算的時候,計算被取消或者失敗,我們需要從緩存中將FutureTask移除。
本文的例子可以參考https://github.com/ddean2009/learn-java-concurrency/tree/master/MemoizedCalculate
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持好吧啦網(wǎng)。
相關(guān)文章:
1. ASP.NET MVC實現(xiàn)區(qū)域或城市選擇2. idea設(shè)置提示不區(qū)分大小寫的方法3. 一個用xslt樣式將xml解析為xhtml的類TransformBinder(兼容FF和IE7.0)4. 解決docker與vmware的沖突問題5. JSP動態(tài)實現(xiàn)web網(wǎng)頁登陸和注冊功能6. Java中有關(guān)Null的9件事7. Java對象布局(JOL)實現(xiàn)過程解析8. php+ajax實現(xiàn)文件切割上傳功能示例9. vue使用Sass時報錯問題的解決方法10. JavaWeb Servlet中Filter過濾器的詳解
