緩存淘汰算法FIFO走越、LRU、LFU及Java實現(xiàn)

緩存淘汰算法
在高并發(fā)耻瑟、高性能的質(zhì)量要求不斷提高時旨指,我們首先會想到的就是利用緩存予以應對。

第一次請求時把計算好的結(jié)果存放在緩存中喳整,下次遇到同樣的請求時谆构,把之前保存在緩存中的數(shù)據(jù)直接拿來使用。

但是框都,緩存的空間一般都是有限搬素,不可能把所有的結(jié)果全部保存下來。那么魏保,當緩存空間全部被占滿再有新的數(shù)據(jù)需要被保存熬尺,就要決定刪除原來的哪些數(shù)據(jù)。如何做這樣決定需要使用緩存淘汰算法谓罗。

常用的緩存淘汰算法有:FIFO粱哼、LRU、LFU檩咱,下面我們就逐一介紹一下揭措。

FIFO
FIFO,F(xiàn)irst In First Out刻蚯,先進先出算法蜂筹。判斷被存儲的時間,離目前最遠的數(shù)據(jù)優(yōu)先被淘汰芦倒。簡單地說艺挪,先存入緩存的數(shù)據(jù),先被淘汰。

最早存入緩存的數(shù)據(jù)麻裳,其不再被使用的可能性比剛存入緩存的可能性大口蝠。建立一個FIFO隊列,記錄所有在緩存中的數(shù)據(jù)津坑。當一條數(shù)據(jù)被存入緩存時妙蔗,就把它插在隊尾上。需要被淘汰的數(shù)據(jù)一直在隊列頭疆瑰。這種算法只是在按線性順序訪問數(shù)據(jù)時才是理想的眉反,否則效率不高。因為那些常被訪問的數(shù)據(jù)穆役,往往在緩存中也停留得最久寸五,結(jié)果它們卻因變“老”而不得不被淘汰出去。

FIFO算法用隊列實現(xiàn)就可以了耿币,這里就不做代碼實現(xiàn)了梳杏。

LRU
LRU,Least Recently Used淹接,最近最少使用算法十性。判斷最近被使用的時間,目前最遠的數(shù)據(jù)優(yōu)先被淘汰塑悼。簡單地說劲适,LRU 的淘汰規(guī)則是基于訪問時間。

如果一個數(shù)據(jù)在最近一段時間沒有被使用到厢蒜,那么可以認為在將來它被使用的可能性也很小霞势。因此,當緩存空間滿時郭怪,最久沒有使用的數(shù)據(jù)最先被淘汰支示。

在Java中,其實LinkedHashMap已經(jīng)實現(xiàn)了LRU緩存淘汰算法鄙才,需要在構(gòu)造函數(shù)第三個參數(shù)傳入true颂鸿,表示按照時間順序訪問≡茆郑可以直接繼承LinkedHashMap來實現(xiàn)嘴纺。

package one.more;

import java.util.LinkedHashMap;
import java.util.Map;

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

    /**
     * 容量限制
     */
    private int capacity;

    LruCache(int capacity) {
        // 初始大小,0.75是裝載因子浓冒,true是表示按照訪問時間排序
        super(capacity, 0.75f, true);
        //緩存最大容量
        this.capacity = capacity;
    }

    /**
     * 重寫removeEldestEntry方法栽渴,如果緩存滿了,則把鏈表頭部第一個節(jié)點和對應的數(shù)據(jù)刪除稳懒。
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

我寫一個簡單的程序測試一下:

package one.more;

public class TestApp {

    public static void main(String[] args) {
        LruCache<String, String> cache = new LruCache(3);
        cache.put("keyA", "valueA");
        System.out.println("put keyA");
        System.out.println(cache);
        System.out.println("=========================");

        cache.put("keyB", "valueB");
        System.out.println("put keyB");
        System.out.println(cache);
        System.out.println("=========================");

        cache.put("keyC", "valueC");
        System.out.println("put keyC");
        System.out.println(cache);
        System.out.println("=========================");

        cache.get("keyA");
        System.out.println("get keyA");
        System.out.println(cache);
        System.out.println("=========================");

        cache.put("keyD", "valueD");
        System.out.println("put keyD");
        System.out.println(cache);
    }
}

運行結(jié)果如下:

put keyA
{keyA=valueA}
=========================
put keyB
{keyA=valueA, keyB=valueB}
=========================
put keyC
{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
{keyC=valueC, keyA=valueA, keyD=valueD}

當然闲擦,這個不是面試官想要的,也不是我們想要的。我們可以使用雙向鏈表和哈希表進行實現(xiàn)墅冷,哈希表用于存儲對應的數(shù)據(jù)纯路,雙向鏈表用于數(shù)據(jù)被使用的時間先后順序。

在訪問數(shù)據(jù)時寞忿,如果數(shù)據(jù)已存在緩存中驰唬,則把該數(shù)據(jù)的對應節(jié)點移到鏈表尾部。如此操作腔彰,在鏈表頭部的節(jié)點則是最近最少使用的數(shù)據(jù)叫编。

當需要添加新的數(shù)據(jù)到緩存時,如果該數(shù)據(jù)已存在緩存中霹抛,則把該數(shù)據(jù)對應的節(jié)點移到鏈表尾部搓逾;如果不存在,則新建一個對應的節(jié)點上炎,放到鏈表尾部恃逻;如果緩存滿了雏搂,則把鏈表頭部第一個節(jié)點和對應的數(shù)據(jù)刪除藕施。

package one.more;

import java.util.HashMap;
import java.util.Map;

public class LruCache<K, V> {

    /**
     * 頭結(jié)點
     */
    private Node head;
    /**
     * 尾結(jié)點
     */
    private Node tail;
    /**
     * 容量限制
     */
    private int capacity;
    /**
     * key和數(shù)據(jù)的映射
     */
    private Map<K, Node> map;

    LruCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
    }

    public V put(K key, V value) {
        Node node = map.get(key);
        // 數(shù)據(jù)存在,將節(jié)點移動到隊尾
        if (node != null) {
            V oldValue = node.value;
            //更新數(shù)據(jù)
            node.value = value;
            moveToTail(node);
            return oldValue;
        } else {
            Node newNode = new Node(key, value);
            // 數(shù)據(jù)不存在凸郑,判斷鏈表是否滿
            if (map.size() == capacity) {
                // 如果滿裳食,則刪除隊首節(jié)點,更新哈希表
                map.remove(removeHead().key);
            }
            // 放入隊尾節(jié)點
            addToTail(newNode);
            map.put(key, newNode);
            return null;
        }
    }

    public V get(K key) {
        Node node = map.get(key);
        if (node != null) {
            moveToTail(node);
            return node.value;
        }
        return null;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("LruCache{");
        Node curr = this.head;
        while (curr != null) {
            if(curr != this.head){
                sb.append(',').append(' ');
            }
            sb.append(curr.key);
            sb.append('=');
            sb.append(curr.value);
            curr = curr.next;
        }
        return sb.append('}').toString();
    }

    private void addToTail(Node newNode) {
        if (newNode == null) {
            return;
        }
        if (head == null) {
            head = newNode;
            tail = newNode;
        } else {
            //連接新節(jié)點
            tail.next = newNode;
            newNode.pre = tail;
            //更新尾節(jié)點指針為新節(jié)點
            tail = newNode;
        }
    }

    private void moveToTail(Node node) {
        if (tail == node) {
            return;
        }
        if (head == node) {
            head = node.next;
            head.pre = null;
        } else {
            //調(diào)整雙向鏈表指針
            node.pre.next = node.next;
            node.next.pre = node.pre;
        }
        node.pre = tail;
        node.next = null;
        tail.next = node;
        tail = node;
    }

    private Node removeHead() {
        if (head == null) {
            return null;
        }
        Node res = head;
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            head = res.next;
            head.pre = null;
            res.next = null;
        }
        return res;
    }

    class Node {
        K key;
        V value;
        Node pre;
        Node next;

        Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
}

再次運行測試程序芙沥,結(jié)果如下:

put keyA
LruCache{keyA=valueA}
=========================
put keyB
LruCache{keyA=valueA, keyB=valueB}
=========================
put keyC
LruCache{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
LruCache{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
LruCache{keyC=valueC, keyA=valueA, keyD=valueD}

LFU

LFU诲祸,Least Frequently Used,最不經(jīng)常使用算法而昨,在一段時間內(nèi)救氯,數(shù)據(jù)被使用次數(shù)最少的,優(yōu)先被淘汰歌憨。簡單地說着憨,LFU 的淘汰規(guī)則是基于訪問次數(shù)。

如果一個數(shù)據(jù)在最近一段時間很少被使用到务嫡,那么可以認為在將來它被使用的可能性也很小甲抖。因此,當空間滿時心铃,最小頻率使用的數(shù)據(jù)最先被淘汰准谚。

我們可以使用雙哈希表進行實現(xiàn),一個哈希表用于存儲對應的數(shù)據(jù)去扣,另一個哈希表用于存儲數(shù)據(jù)被使用次數(shù)和對應的數(shù)據(jù)柱衔。

package one.more;

import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class LfuCache<K, V> {

    /**
     * 容量限制
     */
    private int capacity;

    /**
     * 當前最小使用次數(shù)
     */
    private int minUsedCount;

    /**
     * key和數(shù)據(jù)的映射
     */
    private Map<K, Node> map;
    /**
     * 數(shù)據(jù)頻率和對應數(shù)據(jù)組成的鏈表
     */
    private Map<Integer, List<Node>> usedCountMap;

    public LfuCache(int capacity) {
        this.capacity = capacity;
        this.minUsedCount = 1;
        this.map = new HashMap<>();
        this.usedCountMap = new HashMap<>();
    }

    public V get(K key) {

        Node node = map.get(key);
        if (node == null) {
            return null;
        }
        // 增加數(shù)據(jù)的訪問頻率
        addUsedCount(node);
        return node.value;
    }

    public V put(K key, V value) {
        Node node = map.get(key);
        if (node != null) {
            // 如果存在則增加該數(shù)據(jù)的訪問頻次
            V oldValue = node.value;
            node.value = value;
            addUsedCount(node);
            return oldValue;
        } else {
            // 數(shù)據(jù)不存在,判斷鏈表是否滿
            if (map.size() == capacity) {
                // 如果滿,則刪除隊首節(jié)點唆铐,更新哈希表
                List<Node> list = usedCountMap.get(minUsedCount);
                Node delNode = list.get(0);
                list.remove(delNode);
                map.remove(delNode.key);
            }
            // 新增數(shù)據(jù)并放到數(shù)據(jù)頻率為1的數(shù)據(jù)鏈表中
            Node newNode = new Node(key, value);
            map.put(key, newNode);
            List<Node> list = usedCountMap.get(1);
            if (list == null) {
                list = new LinkedList<>();
                usedCountMap.put(1, list);
            }

            list.add(newNode);
            minUsedCount = 1;
            return null;
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("LfuCache{");
        List<Integer> usedCountList = this.usedCountMap.keySet().stream().collect(Collectors.toList());
        usedCountList.sort(Comparator.comparingInt(i -> i));
        int count = 0;
        for (int usedCount : usedCountList) {
            List<Node> list = this.usedCountMap.get(usedCount);
            if (list == null) {
                continue;
            }
            for (Node node : list) {
                if (count > 0) {
                    sb.append(',').append(' ');
                }
                sb.append(node.key);
                sb.append('=');
                sb.append(node.value);
                sb.append("(UsedCount:");
                sb.append(node.usedCount);
                sb.append(')');
                count++;
            }
        }
        return sb.append('}').toString();
    }

    private void addUsedCount(Node node) {
        List<Node> oldList = usedCountMap.get(node.usedCount);
        oldList.remove(node);

        // 更新最小數(shù)據(jù)頻率
        if (minUsedCount == node.usedCount && oldList.isEmpty()) {
            minUsedCount++;
        }

        node.usedCount++;
        List<Node> set = usedCountMap.get(node.usedCount);
        if (set == null) {
            set = new LinkedList<>();
            usedCountMap.put(node.usedCount, set);
        }
        set.add(node);
    }

    class Node {

        K key;
        V value;
        int usedCount = 1;

        Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

再次運行測試程序捶码,結(jié)果如下:

put keyA
LfuCache{keyA=valueA(UsedCount:1)}
=========================
put keyB
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1)}
=========================
put keyC
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1)}
=========================
get keyA
LfuCache{keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1), keyA=valueA(UsedCount:2)}
=========================
put keyD
LfuCache{keyC=valueC(UsedCount:1), keyD=valueD(UsedCount:1), keyA=valueA(UsedCount:2)}

總結(jié)
看到這里,你已經(jīng)超越了大多數(shù)人或链!

FIFO惫恼,F(xiàn)irst In First Out,先進先出算法澳盐。判斷被存儲的時間祈纯,離目前最遠的數(shù)據(jù)優(yōu)先被淘汰,可以使用隊列實現(xiàn)叼耙。
LRU腕窥,Least Recently Used,最近最少使用算法筛婉。判斷最近被使用的時間簇爆,目前最遠的數(shù)據(jù)優(yōu)先被淘汰,可以使用雙向鏈表和哈希表實現(xiàn)爽撒。
LFU入蛆,Least Frequently Used,最不經(jīng)常使用算法硕勿,在一段時間內(nèi)哨毁,數(shù)據(jù)被使用次數(shù)最少的,優(yōu)先被淘汰源武,可以使用雙哈希表實現(xiàn)扼褪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市粱栖,隨后出現(xiàn)的幾起案子话浇,更是在濱河造成了極大的恐慌,老刑警劉巖闹究,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幔崖,死亡現(xiàn)場離奇詭異,居然都是意外死亡跋核,警方通過查閱死者的電腦和手機岖瑰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砂代,“玉大人蹋订,你說我怎么就攤上這事】桃粒” “怎么了露戒?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵椒功,是天一觀的道長。 經(jīng)常有香客問我智什,道長动漾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任荠锭,我火速辦了婚禮旱眯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘证九。我一直安慰自己删豺,他們只是感情好,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布愧怜。 她就那樣靜靜地躺著呀页,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拥坛。 梳的紋絲不亂的頭發(fā)上遂铡,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天攒射,我揣著相機與錄音膘婶,去河邊找鬼罚舱。 笑死,一個胖子當著我的面吹牛惨奕,可吹牛的內(nèi)容都是我干的雪位。 我是一名探鬼主播竭钝,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼梨撞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了香罐?” 一聲冷哼從身側(cè)響起卧波,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎庇茫,沒想到半個月后港粱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡旦签,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年查坪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宁炫。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡偿曙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出羔巢,到底是詐尸還是另有隱情望忆,我是刑警寧澤罩阵,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站启摄,受9級特大地震影響稿壁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜歉备,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一傅是、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蕾羊,春花似錦落午、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吸申,卻和暖如春梗劫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背截碴。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工梳侨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人日丹。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓走哺,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哲虾。 傳聞我的和親對象是個殘疾皇子丙躏,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

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