java緩存(三)——實(shí)現(xiàn)一個(gè)固定大小的對(duì)象緩存池

本文將介紹使用java語(yǔ)言實(shí)現(xiàn)一個(gè)對(duì)象緩存池挖滤。一步步的實(shí)現(xiàn)包括高速命中,固定大小的緩存隊(duì)列等功能浅役。

這一期我們終于能夠動(dòng)手編寫(xiě)一些代碼斩松,使用java來(lái)實(shí)現(xiàn)一個(gè)在內(nèi)存中的對(duì)象緩存池。

不限大小的高速緩存池

最開(kāi)始的需求是實(shí)現(xiàn)一個(gè)能夠在單線程模式下觉既,根據(jù)唯一主鍵key來(lái)緩存對(duì)象的功能惧盹。
對(duì)于java的集合類來(lái)說(shuō),能夠得到近似的存取時(shí)間復(fù)雜度為O(1)的數(shù)據(jù)結(jié)構(gòu)就是HashMap了瞪讼,此處我們不再講述其數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)钧椰,簡(jiǎn)單的一段代碼實(shí)現(xiàn)此功能:

public class ObjectCache {

    private Map<String, Object> cache;

    public ObjectCache() {
        cache = new HashMap<>();
    }

    public void put(String key, Object value) {
        cache.put(key, value);
    }

    public Object get(String key) {
        return cache.get(key);
    }

}

限制大小的高速緩存池

JVM的堆內(nèi)存大小是有限的,如果一個(gè)緩存沒(méi)有退出機(jī)制符欠,永遠(yuǎn)只能往里面加入對(duì)象的話嫡霞,那么最終就會(huì)導(dǎo)致堆內(nèi)存溢出錯(cuò)誤。所以一般來(lái)說(shuō)我們都要限制緩存池的大小希柿,以免內(nèi)存耗盡诊沪。
那么當(dāng)緩存對(duì)象達(dá)到最大限制大小后,用什么機(jī)制來(lái)淘汰過(guò)期的緩存對(duì)象呢曾撤?常用的有如下策略:

  • FIFO
    此策略根據(jù)寫(xiě)入的時(shí)間排序端姚,當(dāng)需要淘汰時(shí),首先淘汰最早寫(xiě)入的對(duì)象挤悉。
  • LRU
    此策略根據(jù)最后讀取的時(shí)間排序渐裸,當(dāng)需要淘汰時(shí),首先淘汰最后讀取時(shí)間最早的對(duì)象装悲。
  • LFU
    此策略根據(jù)一段時(shí)間窗口內(nèi)昏鹃,總的讀取次數(shù)排序,當(dāng)需要淘汰時(shí)衅斩,首先淘汰時(shí)間窗口內(nèi)讀取次數(shù)最少的對(duì)象盆顾。

其中LFU實(shí)現(xiàn)比較復(fù)雜,需要使用滑窗計(jì)數(shù)器來(lái)幫助實(shí)現(xiàn)畏梆,后續(xù)會(huì)單獨(dú)一篇文章來(lái)介紹此算法。這次我們先來(lái)了解比較簡(jiǎn)單的FIFO和LRU算法的實(shí)現(xiàn)。
我們最開(kāi)始使用的HashMap是無(wú)序的奠涌,所以無(wú)法單獨(dú)來(lái)實(shí)現(xiàn)讀取或者寫(xiě)入的排序宪巨。我們考慮此場(chǎng)景,F(xiàn)IFO需要每次寫(xiě)入或者更新的時(shí)候都改變排序溜畅,而LRU每次讀取的時(shí)候要改變排序捏卓,所以我們就需要一個(gè)能夠排序的,而且很快速改變某個(gè)節(jié)點(diǎn)位置的數(shù)據(jù)結(jié)構(gòu)慈格。那么當(dāng)然我們會(huì)想到LinkedList鏈表數(shù)據(jù)結(jié)構(gòu)怠晴,其插入節(jié)點(diǎn)的時(shí)間復(fù)雜度為O(1)且能夠保持節(jié)點(diǎn)次序,但是單獨(dú)的LinkedList的查詢時(shí)間復(fù)雜度又是O(N)浴捆,超出我們預(yù)期蒜田。所以此處我們需要將其結(jié)合使用,在使用HashMap提供高速查詢寫(xiě)入的同時(shí)选泻,又使用LinkedList來(lái)維護(hù)其插入或者最后讀取的次序冲粤,同時(shí)我們?cè)贖ashMap和LinkedList里維護(hù)同一個(gè)對(duì)象的引用,這樣整體的存儲(chǔ)空間保持基本不變页眯。

其實(shí)JDK在1.7之后已經(jīng)為我們提供了這樣的數(shù)據(jù)結(jié)構(gòu):java.util.LinkedHashMap<K,V>
LinkedHashMap直接繼承自HashMap類梯捕,同時(shí)在內(nèi)部維護(hù)了一個(gè)雙向鏈表,其實(shí)現(xiàn)為:


image.png

可以看到其內(nèi)部的鏈表類LinkedHashMap.Entry繼承自HashMap.Node窝撵,同時(shí)也實(shí)現(xiàn)了Map.Entry接口傀顾,這樣就能在直接在鏈表中使用HashMap中的Node對(duì)象,從而保持同一個(gè)對(duì)象引用碌奉。
在LinkedHashMap.Entry類中短曾,其before和after屬性分別指向當(dāng)前節(jié)點(diǎn)的前節(jié)點(diǎn)和后節(jié)點(diǎn),而LinkedHashMap中也通過(guò)屬性head和tail維護(hù)了此鏈表的頭節(jié)點(diǎn)和尾節(jié)點(diǎn):

    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

LinkedHashMap通過(guò)重寫(xiě)了HashMap中創(chuàng)建節(jié)點(diǎn)的一些方法來(lái)在新增節(jié)點(diǎn)時(shí)維護(hù)鏈表數(shù)據(jù):

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        LinkedHashMap.Entry<K,V> t =
            new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }

    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
        linkNodeLast(p);

這里可以看到道批,在HashMap的EntrySet數(shù)組中错英,根據(jù)hash碰撞的命中數(shù)量,采用鏈表和紅黑樹(shù)兩種節(jié)點(diǎn)結(jié)構(gòu)(JDK1.8以后)隆豹,分別用newNode和newTreeNode插入節(jié)點(diǎn)椭岩,每次都把新的節(jié)點(diǎn)放在LinkedHashMap雙向鏈表的最末尾。

同時(shí)我們來(lái)看HashMap中璃赡,在1.7之后為了LinkedHashMap提供了三個(gè)回調(diào)方法判哥,其在HashMap中的默認(rèn)實(shí)現(xiàn)為空:

    // Callbacks to allow LinkedHashMap post-actions
    // 訪問(wèn)節(jié)點(diǎn)的值后調(diào)用
    void afterNodeAccess(Node<K,V> p) { }
    // 插入新的節(jié)點(diǎn)后調(diào)用
    void afterNodeInsertion(boolean evict) { }
    // 刪除節(jié)點(diǎn)后調(diào)用
    void afterNodeRemoval(Node<K,V> p) { }

而LinkedHashMap在繼承HashMap后重寫(xiě)這三個(gè)方法:

    // 刪除節(jié)點(diǎn)后被HashMap回調(diào)
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

    // 插入新的節(jié)點(diǎn)后被HashMap回調(diào)
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    // 訪問(wèn)節(jié)點(diǎn)的值后被HashMap回調(diào)
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

可以看到其中主要就是一些經(jīng)典的鏈表操作,更新其次序碉考;我們注意到accessOrder屬性塌计,其為true后才會(huì)在訪問(wèn)節(jié)點(diǎn)對(duì)象后更新其次序,我們來(lái)看其在LinkedHashMap中的定義:

    /**
     * The iteration ordering method for this linked hash map: true
     * for access-order, false for insertion-order.
     *  
     * @serial
     */
    final boolean accessOrder;

也就是當(dāng)accessOrder為true時(shí)鏈表采用訪問(wèn)次序排序侯谁,為false時(shí)采用插入次序排序锌仅。其值在LinkedHashMap的構(gòu)造函數(shù)中寫(xiě)入:

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        ……
    }

我們?cè)賮?lái)看afterNodeInsertion回調(diào)方法中調(diào)用的方法:removeEldestEntry章钾,其默認(rèn)實(shí)現(xiàn)永遠(yuǎn)返回false,那么就是說(shuō)其實(shí)LinkedHashMap不會(huì)自動(dòng)刪除過(guò)期節(jié)點(diǎn)热芹,需要我們自己繼承后實(shí)現(xiàn)贱傀。
好了,既然如此伊脓,我們就來(lái)繼承它來(lái)實(shí)現(xiàn)一個(gè)固定大小的對(duì)象緩存池吧:

public class FixedSizeCache<K, V> extends LinkedHashMap<K, V> {

    /**
     * 緩存池的最大大小
     */
    private int maxSize = 0;

    public FixedSizeCache(int initialCapacity,
                          float loadFactor,
                          boolean accessOrder,
                          int maxSize) {
        super(initialCapacity, loadFactor, accessOrder);
        this.maxSize = maxSize;
    }


    /**
     * 當(dāng)前緩存大小已經(jīng)大于maxSize后返回true府寒,在新增節(jié)點(diǎn)后會(huì)刪除一個(gè)最老的節(jié)點(diǎn)
     * 
     * @param eldest
     * @return
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > maxSize;
    }
    
}

好了,如此簡(jiǎn)單报腔,當(dāng)accessOrder為true時(shí)就是一個(gè)LRU緩存池株搔,當(dāng)為false時(shí)就是一個(gè)FIFO緩存池。當(dāng)然此緩存池不保證線程安全纯蛾,只能在單線程下使用了纤房。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市茅撞,隨后出現(xiàn)的幾起案子帆卓,更是在濱河造成了極大的恐慌,老刑警劉巖米丘,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剑令,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡拄查,警方通過(guò)查閱死者的電腦和手機(jī)吁津,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)堕扶,“玉大人碍脏,你說(shuō)我怎么就攤上這事∩运悖” “怎么了典尾?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)糊探。 經(jīng)常有香客問(wèn)我钾埂,道長(zhǎng),這世上最難降的妖魔是什么科平? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任褥紫,我火速辦了婚禮,結(jié)果婚禮上瞪慧,老公的妹妹穿的比我還像新娘髓考。我一直安慰自己,他們只是感情好弃酌,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布氨菇。 她就那樣靜靜地躺著儡炼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪门驾。 梳的紋絲不亂的頭發(fā)上射赛,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天多柑,我揣著相機(jī)與錄音奶是,去河邊找鬼。 笑死竣灌,一個(gè)胖子當(dāng)著我的面吹牛聂沙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播初嘹,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼及汉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了屯烦?” 一聲冷哼從身側(cè)響起坷随,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驻龟,沒(méi)想到半個(gè)月后温眉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翁狐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年类溢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片露懒。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闯冷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出懈词,到底是詐尸還是另有隱情蛇耀,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布坎弯,位于F島的核電站纺涤,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏荞怒。R本人自食惡果不足惜洒琢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望褐桌。 院中可真熱鬧衰抑,春花似錦、人聲如沸荧嵌。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至谭网,卻和暖如春汪厨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背愉择。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工劫乱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锥涕。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓衷戈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親层坠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子殖妇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 九種基本數(shù)據(jù)類型的大小,以及他們的封裝類破花。(1)九種基本數(shù)據(jù)類型和封裝類 (2)自動(dòng)裝箱和自動(dòng)拆箱 什么是自動(dòng)裝箱...
    關(guān)瑋琳l(shuí)inSir閱讀 1,881評(píng)論 0 47
  • Java SE 基礎(chǔ): 封裝谦趣、繼承、多態(tài) 封裝: 概念:就是把對(duì)象的屬性和操作(或服務(wù))結(jié)合為一個(gè)獨(dú)立的整體座每,并盡...
    Jayden_Cao閱讀 2,103評(píng)論 0 8
  • 本系列出于AWeiLoveAndroid的分享前鹅,在此感謝,再結(jié)合自身經(jīng)驗(yàn)查漏補(bǔ)缺尺栖,完善答案嫡纠。以成系統(tǒng)。 Java基...
    濟(jì)公大將閱讀 1,524評(píng)論 1 6
  • 接口/抽象類意義規(guī)范延赌、擴(kuò)展除盏、回調(diào)為其子類提供一個(gè)公共的類型 封裝子類中得重復(fù)內(nèi)容 定義抽象方法,子類雖然有不同的實(shí)...
    MigrationUK閱讀 2,160評(píng)論 1 28
  • 理論總結(jié) 它要解決什么樣的問(wèn)題挫以? 數(shù)據(jù)的訪問(wèn)者蠕、存取、計(jì)算太慢掐松、太不穩(wěn)定踱侣、太消耗資源,同時(shí)大磺,這樣的操作存在重復(fù)性抡句。因...
    jiangmo閱讀 2,840評(píng)論 0 11