LinkedHashMap 实现LRU缓存淘汰机制

文章目录

  • Java 代码
  • 细节分析
    • 与HashMap 的关系
    • removeEldestEntry 方法的使用
  • 运行测试
    • 运行结果

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个条目的稳定状态。
     *
     *   private static final int MAX_ENTRIES = 100;
     *
     *   protected boolean removeEldestEntry(Map.Entry eldest) {
     *      return size() > MAX_ENTRIES;
     *   }
     * 
* 该方法通常不会以任何方式修改地图,而是允许地图按其返回值的指示进行修改。 * 它被允许用于此方法来直接修改地图,但如果这样做的话,它必须返回false(指示地图不应试图任何进一步的修改)。 * 从该方法中修改地图之后返回true的效果是未指定的。 * * 这个实现只返回false (这样,这个地图就像一个法线贴图 - 最老的元素永远不会被删除)。 *

* 以上来自Java API文档中的 LinkedHashMap 该方法的注释翻译。 * 在重写该方法后,返回的结果参考于API中的示例使用。 *

* * @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 方法进行获取过。而在这些元素列表的前边排着的,都是最早进入该缓存的那些元素。

你可能感兴趣的:(java练习)