LinkedHashMap 實現LRU緩存淘汰機制

Java 代碼

package org.feng.lru;

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

/**
 * 最近最少訪問的map
 *
 * @author Feng
 * @date 2020/5/16 14:05
 */
public class LruLinkedHashMap<K,V> extends LinkedHashMap<K,V> {
    private static final long serialVersionUID = -5672516463189218122L;
    /**容量*/
    private int capacity;

    /**
     * 構造一個緩存容器
     * @param capacity 容量(參數的指定參考 {@link java.util.HashMap})
     */
    public LruLinkedHashMap(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    /**
     * 構造一個緩存容器,默認緩存大小爲 16
     */
    public LruLinkedHashMap() {
        this(1 << 4);
    }

    /**
     * 如果此地圖應該刪除其最老的條目,則返回true 。 在將新條目插入到地圖中之後,此方法由put和putAll調用。
     * 它爲實施者提供每次添加新的條目時刪除最老條目的機會。
     * 如果地圖代表一個緩存,這是非常有用的:它允許地圖通過刪除陳舊的條目來減少內存消耗。
     * 示例使用:此覆蓋將允許地圖長達100個條目,然後每次添加新條目時刪除最老條目,保持100個條目的穩定狀態。
     *<pre>
     *   private static final int MAX_ENTRIES = 100;
     *
     *   protected boolean removeEldestEntry(Map.Entry eldest) {
     *      return size() > MAX_ENTRIES;
     *   }
     * </pre>
     * 該方法通常不會以任何方式修改地圖,而是允許地圖按其返回值的指示進行修改。
     * 它被允許用於此方法來直接修改地圖,但如果這樣做的話,它必須返回false(指示地圖不應試圖任何進一步的修改)。
     * 從該方法中修改地圖之後返回true的效果是未指定的。
     *
     * 這個實現只返回false (這樣,這個地圖就像一個法線貼圖 - 最老的元素永遠不會被刪除)。
     * <p>
     * 以上來自Java API文檔中的 LinkedHashMap 該方法的註釋翻譯。
     * 在重寫該方法後,返回的結果參考於API中的示例使用。
     * </p>
     *
     * @param eldest 地圖中最近插入的條目,或者如果這是訪問順序的地圖,最近訪問的條目。
     *              這是將被刪除的條目,此方法返回true 。
     *               如果在put或putAll調用之前地圖爲空,導致此調用,則將是剛插入的條目;
     *               換句話說,如果地圖包含單個條目,則最長條目也是最新的條目。
     * @return 在新插入數據時,返回true 就刪除;false 就不刪除
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }

    public int getCapacity() {
        return capacity;
    }
}

細節分析

與HashMap 的關係

本案例使用 LinkedHashMap ,其底層還是用的HashMap。
因此,在定義初始容量、加載因子這些時,需要參考HashMap。

removeEldestEntry 方法的使用

這個方法是自動調用的。
筆者在HashMap中找到這幾個方法:

// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

是的,你沒看錯,這是空實現,它們作爲鉤子方法,爲 LinkedHashMap 服務。
咱們核心看void afterNodeInsertion(boolean evict) { } 這個方法。

再看看LinkedHashMap 是如何處理這個鉤子方法的:

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);
    }
}

可以看出,這個方法目的是爲了刪除最老的元素,當 removeEldestEntry(first)返回值爲 true 時,就是要刪除了。

此時,我們再回過頭,看看這個鉤子怎麼調用起來的?
最終發現,在這個putVal 方法中的最後,是調用了這個鉤子方法的。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict){
     ...
     afterNodeInsertion(evict);
     ...
}

因此,我們就懂了,removeEldestEntry 這個方法的實現,爲什麼寫成 size() > capacity 就能夠實現 LRU 了。
當該條件成立時,說明當前容器中的元素個數超過了容量了,就是需要刪除的。不成立時,自然就是還沒達到容量,緩存沒滿,還能放。

運行測試

package org.feng.lru;

/**
 * 運行
 *
 * @author Feng
 * @date 2020/5/16 14:33
 */
public class LruClient {
    public static void main(String[] args) {
        // 指定容量爲10
        LruLinkedHashMap<Integer, Integer> map = new LruLinkedHashMap<>(10);

        int len = 11;
        for (int i = 1; i <= len; i++) {
            map.put(i,i);
        }

        System.out.println(map.get(1));
        System.out.println(map.get(3));
        map.forEach((key, value) -> System.out.println("key = " + key + ", value = " + value));
    }
}

運行結果

null
3
key = 2, value = 2
key = 4, value = 4
key = 5, value = 5
key = 6, value = 6
key = 7, value = 7
key = 8, value = 8
key = 9, value = 9
key = 10, value = 10
key = 11, value = 11
key = 3, value = 3

結果分析:

首先指定容量爲 10,也就是說,當前緩存中最多能放 10 個元素。
可是,我使用 for 循環存儲了 11 個元素,那麼最先進入的那個元素會被自動刪除掉。
也就是刪除了 key = 1 的那個元素。

然後是使用get 方法進行獲取 key = 1key = 3 的元素,顯而易見,key=1的元素已經刪除了,因此,打印輸出爲 null。key=3的元素對應的value=3,也打印出來了。

最終,進行遍歷當前緩存,發現 3 排到了最後邊,這是因爲之前使用 get 方法進行獲取過。而在這些元素列表的前邊排着的,都是最早進入該緩存的那些元素。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章