Java併發編程基礎構建模塊(06)——高效緩存總結示例

        前面介紹完了併發編程的基礎構建模塊,也就是一些常用的基礎類,這裏做個簡單的例子,很常見的一個功能,緩存,緩存通常情況下看上去比較簡單,實際中各種緩存框架已經讓我們開發非常方便,但是假如自己新寫一個緩存功能呢,是否能做到高效且可伸縮呢?

       下面做一個簡單緩存,用於改進一個計算費時且複雜的函數。先看一下原始功能:

        

        由於這個工具計算方法傳入相同參數的結果也相同(在數學中是普遍的現象,其他情況下不一定),所以可將計算結果緩存起來(放到內存中,也就是放到某個屬性裏面存着),後續再用到的時候就不用重新計算了。首先想到的是放到一個Map中,結果如下:

        

        因爲HashMap不是線程安全的,所以採用了比較保守的方法,整個方法都加上同步了,但這樣就帶來1個問題,同時只有1個線程能執行計算方法,其他線程都只能阻塞,計算方法時間較長,其他線程阻塞時間就很長。顯然不是我們希望得到的結果,我們需要改進一下:

        (某些情況下確實可以,比如要計算的參數種類非常少,短時間內就能計算出全部參數的結果了,以後不會有新的參數了,這樣前期慢點,後續就沒問題了)

        

        上面這段代碼,使用了ConcurrentHashMap消除了HashMap線程不安全的方法,也取消了synchronized同步,性能上來了,多線程併發使用也沒問題了。但還有一個不足,當兩個線程同時傳入相同的參數時,會導致重複執行(業務邏輯執行時間越長概率越大),我們希望的結果是相同參數只計算1次,後續再調用就從緩存中獲取就行了。

        這樣就會引出一個問題,想要只計算1次,多線程還要同時獲取這個結果怎麼辦,不由得想到前面介紹過的一個類:FutureTask。FutureTask線程在計算時,所有其他線程使用get方法獲取值會阻塞,一旦計算完成,其他線程能馬上拿到結果,好,我們改造一下:

        

        這樣看似不錯了,但也沒徹底解決同一個值只執行1次,只是將原來的概率降的非常低而已,假如realCalculate要執行2s,Calculate02中2s內進來的相同方法會都執行,而Calculate03中把時間降到了if(ft== null)中的方法執行時間,也就是說相同值同時執行時,可能同時初始化多個FutureTask,雖然概率很低。防止重複初始化我們自然想到了加鎖,看下面的代碼:

        

        這個鎖有點類似單例模式中的單例初始化的鎖,好了,截至目前看似比較完美了,其實仍然有兩處不足:

        1、 在創建FutureTask前加鎖,鎖的粒度比較大,同步功能越少對性能越好,其實還有更細的加鎖方式,就是使用ConcurrentHashMap內自己的鎖;

        2、 FutureTask存在計算失敗的可能,如果計算線程被取消了怎麼辦,其他等待線程和後續的線程再執行時,這個參數對應的FutureTask已經損壞了,無法拿到結果了,所以當失敗時,需要清除FutureTask,換一個新的。

        好,我們來看下面這個代碼:

        

        這樣看來就完美一些了,這樣不僅高效,而且安全性也非常好,擴展性也不錯。

        其實,再緩存設計中,如果不緩存最終結果,就要控制好緩存對象,否則會造成緩存污染問題,比如上面的程序中,沒緩存最終結果,而是緩存了FutureTask,FutureTask會引來一些其他問題,比如發生某些異常時,取消計算時等情況要清除緩存,這樣緩存才能計算成功。還有緩存逾期問題,每個結果需要指定一個時間,定期掃描逾期元素並刪除等,不過這些是緩存設計方法考慮的,此處例子以併發爲主,不做太多討論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章