Java中LRU的實現

前言

LRU,全稱Least Recently Used,即最近最久未使用算法,用於操做系統的頁面置換算法,以及一些常見的框架。其原理實質就是當須要淘汰數據時,會選擇那些最近沒有使用過的數據進行淘汰,換句話說,當某數據被訪問時,就把其移動到淘汰隊列的隊首(也就是最不會被淘汰的位置)java

實現

基於這樣的原則,咱們就能夠着手實現了。不過Java已經爲咱們提供了一個現成的模板,咱們站在巨人的肩膀上,能夠參考一下Java是如何實現LRU功能的算法

LinkedHashMap

在LinkedHashMap中,有一個不多用到的構造函數:緩存

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
複製代碼

其中accessOrder這一屬性,在其餘的構造函數中是默認爲false的,若是咱們經過該構造函數將其設爲true以後,就實現了LRU功能,下面的程序簡單了作了下演示:app

public static void main(String[] args) {

        int cacheSize = 3;
        // 最大容量 = (緩存大小 / 負載因子)+ 1,保證不會觸發自動擴容
        LinkedHashMap<String, String> cache = new LinkedHashMap<String, String>(
                (int)(cacheSize/ 0.75f) + 1, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > cacheSize;
            }
        };

        cache.put("1", "a");
        cache.put("2", "b");
        cache.put("3", "c");

        // head => "1" => "2" => "3" => null

        // put已存在的值,和get方法是同樣的效果
        cache.put("1", "a");

        // head => "2" => "3" => "1" => null;

        cache.put("4", "d");

        // head => "3" => "1" => "4" => null;
        
        for (String key: cache.keySet()) {
            System.out.println(key);
        }
    }
複製代碼

其實還有很重要的一點,就是須要重寫removeEldestEntry()這一方法,默認是返回false的,當返回true時,會移除最久沒有使用的節點,因此咱們要作的,就是當容量達到緩存限制時,移除LRU算法斷定的最近最久未使用節點框架

能夠看到,咱們依次插入節點一、二、3後,若是此時再插入節點4,就會致使removeEldestEntry()返回爲true,而後移除隊首節點,即節點1。可是咱們這裏因爲中間重複插入了一次節點1,因此會判斷節點1是「常常訪問的節點」,因此節點1被提到鏈表最後,隊首節點就變成了節點2,當容量超過限制時,會把節點2移除ide

實現原理

探索LinkedHashMap中LRU的實現原理,咱們就要追溯到HashMap中的putVal方法,這個方法最後觸發了一個回調函數:函數

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        // ...
        
            if (e != null) { // existing mapping for key
                // ...
                afterNodeAccess(e);
                return oldValue;
            }
        }
        afterNodeInsertion(evict);
        return null;
    }
複製代碼

putVal()方法在插入後會觸發方法的回調,有兩種狀況:this

  • 若是插入的值已存在,則觸發afterNodeAccess(e)
  • 若是插入的值不存在,則觸發afterNodeInsertion(evict)

其中,變量e是「撞車」的節點,變量evict在子類不重寫put()方法的狀況下是默認爲true的,因此咱們就把它看成常量來看spa

而後咱們回到,LinkedHashMap中,來看這個兩個鉤子方法(HashMap中這兩個方法實現均爲空):操作系統

void afterNodeInsertion(boolean evict) {
        LinkedHashMap.Entry<K,V> first;
        // 如下狀況知足時,調用removeNode移除最久未使用的節點:
        // 1. evict爲true
        // 2. 頭結點不爲空
        // 3. 符合移除條件:removeEldestEntry返回true
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    void afterNodeAccess(Node<K,V> e) {
        LinkedHashMap.Entry<K,V> last;
        // 開啓LRU模式,且訪問的節點不是尾節點,則將被訪問的節點置於鏈表尾
        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;
        }
    }
複製代碼

顯而易見,afterNodeInsertion負責在插入以後判斷是否須要移除最近最久未使用的節點(即鏈表頭節點),afterNodeAccess負責在訪問某節點以後,將該節點移動到鏈表尾

afterNodeAccess中,由於要考慮到各類特殊狀況,並且是一個帶有頭尾節點的雙向鏈表,因此狀況判斷比較複雜,實際上就是將指定節點移動到隊尾,若是本身想實現一個相似的功能能夠不作的這麼複雜

總結

通常來講,若是想作一個LRU算法實現的話,LinkedHashMap就能知足須要了。要是想本身實現的話,這裏提供一個實現的思路:

  1. 用鏈表存儲數據
  2. 一個節點被訪問後,將其置於鏈表尾
  3. 鏈表頭結點就是最近最久未使用的節點,直接移除便可
相關文章
相關標籤/搜索