LRU緩存算法

一、什么是緩存

這里說的緩存是一種廣義的概念耍属,在計算機存儲層次結(jié)構(gòu)中矾兜,低一層的存儲器都可以看做是高一層的緩存损趋。比如Cache是內(nèi)存的緩存,內(nèi)存是硬盤的緩存椅寺,硬盤是網(wǎng)絡(luò)的緩存等等浑槽。

緩存可以有效地解決存儲器性能與容量的這對矛盾,但絕非看上去那么簡單返帕。如果緩存算法設(shè)計不當(dāng)桐玻,非但不能提高訪問速度,反而會使系統(tǒng)變得更慢荆萤。

從本質(zhì)上來說镊靴,緩存之所以有效是因為程序和數(shù)據(jù)的局部性(locality)。程序會按固定的順序執(zhí)行链韭,數(shù)據(jù)會存放在連續(xù)的內(nèi)存空間并反復(fù)讀寫偏竟。這些特點使得我們可以緩存那些經(jīng)常用到的數(shù)據(jù),從而提高讀寫速度敞峭。

緩存的大小是固定的踊谋,它應(yīng)該只保存最常被訪問的那些數(shù)據(jù)。然而未來不可預(yù)知旋讹,我們只能從過去的訪問序列做預(yù)測殖蚕,于是就有了各種各樣的緩存替換策略。本文介紹一種簡單的緩存策略骗村,稱為最近最少使用(LRU嫌褪,Least Recently Used)算法呀枢。

二胚股、LRU的實現(xiàn)

我們以內(nèi)存訪問為例解釋緩存的工作原理。假設(shè)緩存的大小固定裙秋,初始狀態(tài)為空琅拌。每發(fā)生一次讀內(nèi)存操作,首先查找待讀取的數(shù)據(jù)是否存在于緩存中摘刑,若是进宝,則緩存命中,返回數(shù)據(jù)枷恕;若否党晋,則緩存未命中,從內(nèi)存中讀取數(shù)據(jù),并把該數(shù)據(jù)添加到緩存中未玻。向緩存添加數(shù)據(jù)時灾而,如果緩存已滿,則需要刪除訪問時間最早的那條數(shù)據(jù)扳剿,這種更新緩存的方法就叫做LRU旁趟。

實現(xiàn)LRU時,我們需要關(guān)注它的讀性能和寫性能庇绽,理想的LRU應(yīng)該可以在O(1)的時間內(nèi)讀取一條數(shù)據(jù)或更新一條數(shù)據(jù)锡搜,也就是說讀寫的時間復(fù)雜度都是O(1)。

此時很容易想到使用HashMap瞧掺,根據(jù)數(shù)據(jù)的鍵訪問數(shù)據(jù)可以達到O(1)的速度耕餐。但是更新緩存的速度卻無法達到O(1),因為需要確定哪一條數(shù)據(jù)的訪問時間最早夸盟,這需要遍歷所有緩存才能找到蛾方。

因此,我們需要一種既按訪問時間排序上陕,又能在常數(shù)時間內(nèi)隨機訪問的數(shù)據(jù)結(jié)構(gòu)桩砰。

這可以通過HashMap+雙向鏈表實現(xiàn)。HashMap保證通過key訪問數(shù)據(jù)的時間為O(1)释簿,雙向鏈表則按照訪問時間的順序依次穿過每個數(shù)據(jù)亚隅。之所以選擇雙向鏈表而不是單鏈表,是為了可以從中間任意結(jié)點修改鏈表結(jié)構(gòu)庶溶,而不必從頭結(jié)點開始遍歷煮纵。

如下圖所示,黑色部分為HashMap的結(jié)構(gòu)偏螺,紅色箭頭則是雙向鏈表的正向連接(逆向連接未畫出)行疏。可以清晰地看到套像,數(shù)據(jù)的訪問順序是1->3->5->6->10酿联。我們只需要在每次訪問過后改變鏈表的連接順序即可。

HashMap+雙向鏈表

實現(xiàn)代碼如下:

/**
 * @author wjg
 * 
 * LRU(Least Recently Used)緩存算法
 * 使用HashMap+雙向鏈表夺巩,使get和put的時間復(fù)雜度達到O(1)贞让。
 * 讀緩存時從HashMap中查找key,更新緩存時同時更新HashMap和雙向鏈表柳譬,雙向鏈表始終按照訪問順序排列喳张。
 *
 */
public class LRUCache {

    /**
     * @param args
     * 測試程序,訪問順序為[[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]美澳,其中成對的數(shù)調(diào)用put销部,單個數(shù)調(diào)用get摸航。
     * get的結(jié)果為[1],[-1],[-1],[3],[4],-1表示緩存未命中舅桩,其它數(shù)字表示命中忙厌。
     */
    public static void main(String[] args) {
        
        LRUCache cache = new LRUCache(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(1));
        cache.put(3, 3);
        System.out.println(cache.get(2));
        cache.put(4, 4);
        System.out.println(cache.get(1));
        System.out.println(cache.get(3));
        System.out.println(cache.get(4));

    }
    
    // 緩存容量
    private final int capacity;
    // 用于加速緩存項隨機訪問性能的HashMap
    private HashMap<Integer, Entry> map;
    // 雙向鏈表頭結(jié)點,該側(cè)的緩存項訪問時間較早
    private Entry head;
    // 雙向鏈表尾結(jié)點江咳,該側(cè)的緩存項訪問時間較新
    private Entry tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<Integer, Entry>((int)(capacity / 0.75 + 1), 0.75f);
        head = new Entry(0, 0);
        tail = new Entry(0, 0);
        head.next = tail;
        tail.prev = head;
    }
    
    /**
     * 從緩存中獲取key對應(yīng)的值逢净,若未命中則返回-1
     * @param key 鍵
     * @return key對應(yīng)的值,若未命中則返回-1
     */
    public int get(int key) {
        if (map.containsKey(key)) {
            Entry entry = map.get(key);
            popToTail(entry);
            return entry.value;
        }
        return -1;
    }
    
    /**
     * 向緩存中插入或更新值
     * @param key 待更新的鍵
     * @param value 待更新的值
     */
    public void put(int key, int value) {
        if (map.containsKey(key)) {
            Entry entry = map.get(key);
            entry.value = value;
            popToTail(entry);
        }
        else {
            Entry newEntry = new Entry(key, value);
            if (map.size() >= capacity) {
                Entry first = removeFirst();
                map.remove(first.key);
            }
            addToTail(newEntry);
            map.put(key, newEntry);
        }
    }
    
    /**
     * 緩存項的包裝類歼指,包含鍵爹土、值、前驅(qū)結(jié)點踩身、后繼結(jié)點
     * @author wjg
     *
     */
    class Entry {
        int key;
        int value;
        Entry prev;
        Entry next;
        
        Entry(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }
    
    // 將entry結(jié)點移動到鏈表末端
    private void popToTail(Entry entry) {
        Entry prev = entry.prev;
        Entry next = entry.next;
        prev.next = next;
        next.prev = prev;
        Entry last = tail.prev;
        last.next = entry;
        tail.prev = entry;
        entry.prev = last;
        entry.next = tail;
    }
    
    // 移除鏈表首端的結(jié)點
    private Entry removeFirst() {
        Entry first = head.next;
        Entry second = first.next;
        head.next = second;
        second.prev = head;
        return first;
    }
    
    // 添加entry結(jié)點到鏈表末端
    private void addToTail(Entry entry) {
        Entry last = tail.prev;
        last.next = entry;
        tail.prev = entry;
        entry.prev = last;
        entry.next = tail;
    }

}

每個方法和成員變量前都有中文注釋胀茵,不必過多解釋。

值得一提的是挟阻,Java API中其實已經(jīng)有數(shù)據(jù)類型提供了我們需要的功能琼娘,就是LinkedHashMap這個類。該類內(nèi)部也是采用HashMap+雙向鏈表實現(xiàn)的附鸽。使用這個類實現(xiàn)LRU就簡練多了脱拼。

/**
 * 
 * 一個更簡單實用的LRUCache方案,使用LinkedHashMap即可實現(xiàn)坷备。
 * LinkedHashMap提供了按照訪問順序排序的方案熄浓,內(nèi)部也是使用HashMap+雙向鏈表。
 * 只需要重寫removeEldestEntry方法省撑,當(dāng)該方法返回true時赌蔑,LinkedHashMap會刪除最舊的結(jié)點。
 * 
 * @author wjg
 *
 */
public class LRUCacheSimple {

    /**
     * @param args
     */
    public static void main(String[] args) {
        LRUCacheSimple cache = new LRUCacheSimple(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(1));
        cache.put(3, 3);
        System.out.println(cache.get(2));
        cache.put(4, 4);
        System.out.println(cache.get(1));
        System.out.println(cache.get(3));
        System.out.println(cache.get(4));
    }
    
    private LinkedHashMap<Integer, Integer> map;
    private final int capacity;
    public LRUCacheSimple(int capacity) {
        this.capacity = capacity;
        map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true){
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > capacity;
            }
        };
    }
    public int get(int key) {
        return map.getOrDefault(key, -1);
    }
    public void put(int key, int value) {
        map.put(key, value);
    }

}

只需要覆寫LinkedHashMap的removeEldestEntry方法竟秫,在緩存已滿的情況下返回true娃惯,內(nèi)部就會自動刪除最老的元素。

感興趣的同學(xué)可以做一下LeetCode 146. LRU Cache這道題肥败,嘗試一下如何實現(xiàn)這個算法趾浅。

完整版代碼LRUCache.java收錄在GitHub倉庫Algorithms中,歡迎大家下載試用拙吉。

參考資料

Cache replacement policies WikiPedia
146. LRU Cache LeetCode
Laziest implementation: Java's LinkedHashMap takes care of everything sky-xu

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末潮孽,一起剝皮案震驚了整個濱河市揪荣,隨后出現(xiàn)的幾起案子筷黔,更是在濱河造成了極大的恐慌,老刑警劉巖仗颈,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佛舱,死亡現(xiàn)場離奇詭異椎例,居然都是意外死亡,警方通過查閱死者的電腦和手機请祖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門订歪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肆捕,你說我怎么就攤上這事刷晋。” “怎么了慎陵?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵眼虱,是天一觀的道長。 經(jīng)常有香客問我席纽,道長捏悬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任润梯,我火速辦了婚禮过牙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘纺铭。我一直安慰自己寇钉,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布舶赔。 她就那樣靜靜地躺著摧莽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪顿痪。 梳的紋絲不亂的頭發(fā)上镊辕,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音蚁袭,去河邊找鬼征懈。 笑死,一個胖子當(dāng)著我的面吹牛揩悄,可吹牛的內(nèi)容都是我干的卖哎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼删性,長吁一口氣:“原來是場噩夢啊……” “哼亏娜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蹬挺,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤维贺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后巴帮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溯泣,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡虐秋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了垃沦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片客给。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肢簿,靈堂內(nèi)的尸體忽然破棺而出靶剑,到底是詐尸還是另有隱情,我是刑警寧澤池充,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布抬虽,位于F島的核電站,受9級特大地震影響纵菌,放射性物質(zhì)發(fā)生泄漏阐污。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一咱圆、第九天 我趴在偏房一處隱蔽的房頂上張望笛辟。 院中可真熱鬧,春花似錦序苏、人聲如沸手幢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽围来。三九已至,卻和暖如春匈睁,著一層夾襖步出監(jiān)牢的瞬間监透,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工航唆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留胀蛮,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓糯钙,卻偏偏與公主長得像粪狼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子任岸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

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

  • 1. LRU 1.1.原理 LRU(Leastrecentlyused再榄,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來...
    安易學(xué)車閱讀 2,532評論 0 23
  • Android緩存淺析 By吳思博 1、引言 2享潜、常見的幾種緩存算法 3困鸥、Android緩存的機制 4、LruCa...
    吳小博Toby閱讀 2,902評論 1 5
  • 理論總結(jié) 它要解決什么樣的問題米碰? 數(shù)據(jù)的訪問窝革、存取、計算太慢吕座、太不穩(wěn)定虐译、太消耗資源,同時吴趴,這樣的操作存在重復(fù)性漆诽。因...
    jiangmo閱讀 2,849評論 0 11
  • 1. LRU 1.1. 原理LRU(Least recently used,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記...
    AKyS佐毅閱讀 2,166評論 0 3
  • 自從Apple向開發(fā)者開放Xcode 7 Beta版本時锣枝,我就下載了Xcode 7 Beta厢拭,安裝Xcode 6....
    雷霆嘎巴嘎嘎閱讀 302評論 0 1