LRU的理解與Java實現


簡介

LRU(Least Recently Used)直譯為“最近最少使用”。其實很多老外發明的詞直譯過來對於我們來說並不是特別好理解,甚至有些詞並不在國人的思維模式之內,比如快速排序中的Pivot,模擬信號中的Analog 等等。筆者認為最好的理解方式就是看他誕生的原因,看這個概念的出現如何一步一步演變為現在的樣子。假如說你自己對某個問題想到了一個解決辦法,於是你按照語義給他起了個名字,假如你直接將這個詞兒說給別人,不知道這個詞兒來歷的人大概很難理解。所以為了力求方便理解,下面我們先來看看LRU是什么,主要是為了解決什么問題。

其實LRU這個概念映射到現實生活中非常好理解,就好比說小明的衣櫃中有很多衣服,假設他的衣服都只能放在這個櫃子里,小明每過一陣子小明就會買新衣服,不久小明的衣櫃就放滿了衣服。這個小明想了個辦法,按照衣服上次穿的時間排序,丟掉最長時間沒有穿過那個。這就是LRU策略。

映射到計算機概念中,上述例子中小明的衣櫃就是內存,而小明的衣服就是緩存數據。我們的內存是有限的。所以當緩存數據在內存越來越多,以至於無法存放即將到來的新緩存數據時,就必須扔掉最不常用的緩存數據。所以對於LRU的抽象總結如下:

  • 緩存的容量是有限的
  • 當緩存容量不足以存放需要緩存的新數據時,必須丟掉最不常用的緩存數據

實現

理解了LRU的原理之后,想要將其轉換為代碼並不是一件很困難的事。我們訪問緩存通常使用一個字符串來定位緩存數據(事實上其他數據形式也沒有關系),這種場景我們反射性的想到HashMap。所以我們先來簡單的定義一下LRUCachce類。

public class LRUCache {
    private HashMap<String, Object> map;
    private int capacity;
    public Object get(String key) {
        return map.get(key);
    }
    public void set(String key, Object value) {
        this.map.put(key, value);
    }
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<String, Object>();
    }
}

這樣我們僅僅是定義了一個容量有限的LRUCache,可以存取數據,但並沒有實現緩存容量不足時丟棄最不常用的數據的功能,而這件事做起來似乎顯得稍微麻煩一些,問題在於我們如何找到最久沒有用的緩存。

一個最容易想到的辦法是我們在給這個緩存數據加一個時間戳,每次get緩存時就更新時間戳,這樣找到最久沒有用的緩存數據問題就能夠解決,但與之而來的會有兩個新問題:

  • 雖然使用時間戳可以找到最久沒用的數據,但我們最少的代價也要將這些緩存數據遍歷一遍,除非我們維持一個按照時間戳排好序的SortedList。
  • 添加時間戳的方式為我們的數據帶來了麻煩,我們並不太好在緩存數據中添加時間戳的標識,這可能需要引入新的包含時間戳的包裝對象。

而且我們的需要只是找到最久沒用使用的緩存數據,並不需要精確的時間。添加時間戳的方式顯然沒有利用這一特性,這就使得這個辦法從邏輯上來講可能不是最好的。

然而辦法總是有的,我們可以維護一個鏈表,當數據每一次查詢就將數據放到鏈表的head,當有新數據添加時也放到head上。這樣鏈表的tail就是最久沒用使用的緩存數據,每次容量不足的時候就可以刪除tail,並將前一個元素設置為tail,顯然這是一個雙向鏈表結構,因此我們定義LRUNode如下:

class LRUNode {
    String key;
    Object value;
    LRUNode prev;
    LRUNode next;
    public LRUNode(String key, Object value) {
        this.key = key;
        this.value = value;
    }
}

而LRUCache的簡單實現最終如下:

public class LRUCache {
    private HashMap<String, LRUNode> map;
    private int capacity;
    private LRUNode head;
    private LRUNode tail;
    public void set(String key, Object value) {
        LRUNode node = map.get(key);
        if (node != null) {
            node = map.get(key);
            node.value = value;
            remove(node, false);
        } else {
            node = new LRUNode(key, value);
            if (map.size() >= capacity) {
                // 每次容量不足時先刪除最久未使用的元素
                remove(tail, true);
            }
            map.put(key, node);
        }
        // 將剛添加的元素設置為head
        setHead(node);
    }
    public Object get(String key) {
        LRUNode node = map.get(key);
        if (node != null) {
            // 將剛操作的元素放到head
            remove(node, false);
            setHead(node);
            return node.value;
        }
        return null;
    }
    private void setHead(LRUNode node) {
        // 先從鏈表中刪除該元素
        if (head != null) {
            node.next = head;
            head.prev = node;
        }
        head = node;
        if (tail == null) {
            tail = node;
        }
    }
    // 從鏈表中刪除此Node,此時要注意該Node是head或者是tail的情形
    private void remove(LRUNode node, boolean flag) {
        if (node.prev != null) {
            node.prev.next = node.next;
        } else {
            head = node.next;
        }
        if (node.next != null) {
            node.next.prev = node.prev;
        } else {
            tail = node.prev;
        }
        node.next = null;
        node.prev = null;
        if (flag) {
            map.remove(node.key);
        }
    }
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<String, LRUNode>();
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM