03_LinkedHashMap源码解读+手写LRU缓存淘汰策略

LRU 复习:最近最少使用,优先删除最久未访问的

1. LinkedHashMap

1.1 基本应用

LinkedHashMapHashMap 的区别:LinkedHashMap 可以让我们根据插入顺序遍历 Map

LinkedHashMap 底层还是数组加链表的形式,只是可以允许我们按照 插入/访问 顺序输出。

public class MapTest {
    public static void main(String[] args) {
        Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
        Map<String, Integer> hashMap = new HashMap<>();

        linkedHashMap.put("A", 10);
        linkedHashMap.put("Z", 20);
        linkedHashMap.put("融易贷", 60);
        linkedHashMap.put("支付宝", 90);

        hashMap.put("A", 10);
        hashMap.put("Z", 20);
        hashMap.put("融易贷", 60);
        hashMap.put("支付宝", 90);
        // 遍历顺序和插入顺序一致
        System.out.println("linkedHashMap" + linkedHashMap.keySet());
        // 无序
        System.out.println("hashMap" +  hashMap.keySet());
    }
}

输出:
linkedHashMap[A, Z, 融易贷, 支付宝]
hashMap[A, 支付宝, 融易贷, Z]

2. 源码解读

2.1 双链表部分

核心就是重写了 newNode 方法,并继承 HashMap.NodenewNode 的时候维护双链表

// 1. LinkedHashMap 继承与 HashMap
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V> {}

// 2. put 方法仍然使用的是 HashMap 中的 put,最终调用的是 HashMap.putVal

// 3. LinkedHashMap 内部类 Entry 继承 HashMap.Node 并增加属性 before, after
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

// 4. 并重写 newNode,也是在这里额外维护一个双向链表的
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 双链表维护的核心方法
    linkNodeLast(p);
    return p;
}

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}
2.1.1 示例
public class OtherTest {
    public static void main(String[] args) {
        Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put(1, "A");
        linkedHashMap.put(2, "B");
        linkedHashMap.put(3, "C");

        System.out.println(linkedHashMap.keySet());
    }
}

注意 beforore 和 after 执向顺序,后续要用。可以看到,整体的添加顺序是 head 不变,tail 后移(head 存储的是最旧的元素)
在这里插入图片描述

2.2 LinkedHashMap accessOrder

支持最终顺序为:true 访问顺序,false 插入顺序。核心代码为:如果访问元素存在则将其在链表中位置移动到 tail 处(最新)
以下源码的阅读注意 before after 指向

这段代码与上述代码唯一的区别就是将 accessOrder 设置为 true

public class OtherTest {
    public static void main(String[] args) {
        Map<Integer, String> linkedHashMap = new LinkedHashMap<>(10, 0.75f, true);
        linkedHashMap.put(1, "A");
        linkedHashMap.put(2, "B");
        linkedHashMap.put(3, "C");

        System.out.println(linkedHashMap.keySet()); // [1, 2, 3]

        linkedHashMap.get(2);
        System.out.println(linkedHashMap.keySet()); // [1, 3, 2]
    }
}

true 访问顺序,false 插入顺序

  /**
   * The iteration ordering method for this linked hash map: <tt>true</tt>
   * for access-order, <tt>false</tt> for insertion-order.
   * 
   * @serial
   */
  final boolean accessOrder;
// get 方法,元素获取 getNode 调用的 HashMap 的方法
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;

	// 位置调换方法
    if (accessOrder)
    	// 这个是核心方法
        afterNodeAccess(e);
    return e.value;
}

元素移动核心代码

// 将访问元素移动到 tail(最新位置处)
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // 如果访问的是 tail 元素不用移动
    if (accessOrder && (last = tail) != e) {
    	/** 等价于
    	 * LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e; // p 访问元素本身
    	 * LinkedHashMap.Entry<K,V> b = p.before;	// b 是访问元素的前一个
    	 * LinkedHashMap.Entry<K,V> a = p.after;	// a 是访问元素的后一个
    	 */
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null; 	// p 要移动到 tail,after 不指向别的元素
        if (b == null)		// b == null 访问的是 head 元素
            head = a;		// head 后移 head = p.after
        else
        	// 否则就是 p 前边有元素,让 p 前后的元素关联起来,自己出去
            b.after = a;	// 等价于:p.before.after = p.after(结合上边的图查看)
         
        // a != null 不是 tail 这个应该一定成立(个人理解,因为访问 tail 不进入)
        if (a != null)
        	// p 要出去,让 p 前后的关联起来
            a.before = b;	// 等价于:p.after.before = p.before
        else
        	// 感觉进不来
            last = b;

		// 感觉这个 last 也永远不为 null
        if (last == null)
            head = p;
        else {
        	// p 移动到 tail 位置,原来的 tail(last)变第二个,维护 指向关系
            p.before = last;
            last.after = p;
        }

		// tail 指向 p,p 移动到最新位置
        tail = p;
        ++modCount;		// 这个只是为了防止迭代的时候进行修改
    }
}

在这里插入图片描述

到这里,是不是感觉与 LRU 很接近了,每次访问将访问的元素提到最前面(LinkedHashMap tail 表示前面),只差元素满的时候自动剔除最旧的数据了(head),我们接着往下看。

2.3 自动剔除

LinkedHashMap 重写了 afterNodeInsertion,只要 removeEldestEntry() 返回 true 就可以对 head 元素进行删除,因此我们如果要实现 LRU 只需要修改 removeEldestEntry 即可。

// HashMap.putVal 的最后有一个函数:afterNodeInsertion(true)

// LinkedHashMap 重写了 afterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove eldest
  LinkedHashMap.Entry<K,V> first;
  	// 只要 head != null(有元素添加到 map)
  	// removeEldestEntry(first) 是否允许删除最旧的,返回 true 表示进行删除
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        // 就会对最旧的 head 元素进行删除
        removeNode(hash(key), key, null, false, true);
    }
}

// 默认 false,即默认不剔除而是像 HashMap 一样一直扩容,只是多维护了一个双链表
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
   return false;
}

我们来看下剔除方法:removeNode结合第一张图片删除中间元素 2 看比较好理解
removeNodeHashMap 的删除 HashMap 节点的方法,这个省略。
removeNode 最后调:afterNodeRemoval 进行链表元素剔除

void afterNodeRemoval(Node<K,V> e) { // unlink
	// 上面讲过,这其实是三句
	// p 删除元素
	// b = p.before
	// a = p.after
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;

	// p 要从链表中移除,前后指针不用指向其他元素
    p.before = p.after = null;
	// b == null 表示 e 为 head,即要删除的是 head
    if (b == null)
    	// 将 head 指向 p 的后面元素 after,head = a = p.after
        head = a;
    else
    	// 删除元素,让元素前后指向跳过 p
        b.after = a;	// p.before.after = p.after

	// a == null 表示删除的是 tail
    if (a == null)
        tail = b;	// tail 指针前移即可
    else
    	// 跳过 p
        a.before = b;	// p.after.before = p.before
}
2.4 顺序遍历

最后我们再看一下为何可以顺序访问元素

可以看到 keySet 返回的是 java.util.LinkedHashMap.LinkedKeySet,我们遍历都是获取 keySet,然后迭代,因此这一步应该就是发生在 keySet 这一步了。
另外 keySet 直接输出:System.out.println(lruDemo.keySet()); 隐式调用的是 toString() 方法,最后还是调用的迭代器遍历,代码就省略了感兴趣的小伙伴自己可以看下,位置:java.util.AbstractCollection#toString

public Set<K> keySet() {
   Set<K> ks = keySet;
   if (ks == null) {
       ks = new LinkedKeySet();
       keySet = ks;
   }
   return ks;
}

LinkedKeySet 的迭代器方法返回的是:java.util.LinkedHashMap.LinkedKeyIterator

public final Iterator<K> iterator() {
   return new LinkedKeyIterator();
}

LinkedKeyIterator 又继承于 java.util.LinkedHashMap.LinkedHashIterator,根据继承会先初始化父类特征,而 LinkedHashIterator 创建时,使用 next 记录了 head

abstract class LinkedHashIterator {
	// 构造
	LinkedHashIterator() {
       next = head;
       expectedModCount = modCount;
       current = null;
   }
}

final class LinkedKeyIterator extends LinkedHashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().getKey(); }
}

可以看到核心就是 nextNode 如何实现的了:可以看到遍历的过程是从 head 开始向后遍历直到 tail,同样是依靠链表来维护顺序的。即 keySet() 索引越靠近 0 的位置是越旧的元素(从 head 往 tail 方向遍历的)

public final boolean hasNext() {
   return next != null;
}

final LinkedHashMap.Entry<K,V> nextNode() {
    // e 就是 head
    LinkedHashMap.Entry<K,V> e = next;
    // 这个是并发监测的,不用管
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    // 没有元素遍历抛异常
    if (e == null)
        throw new NoSuchElementException();
    // 当前元素
    current = e;
    // next 后移
    next = e.after;
    // 返回当前元素
    return e;
}

3. LRU 实现

通过上面 LinkedHashMap 我们可以看到只需要 removeEldestEntry 在元素满的时候返回 true,即可实现 LRU 算法。

3.1 LinkedHashMap 实现 LRU
public class LruDemo<K, V> extends LinkedHashMap<K, V> {
    private int cacheSize;

    public LruDemo(int cacheSize) {
        super(cacheSize, 0.75f, true);
        this.cacheSize = cacheSize;
    }

	// 当 map 长度大于 cacheSize 的时候,将最旧的元素剔除,即可实现LRU
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return this.size() > cacheSize;
    }

    public static void main(String[] args) {
        LruDemo<Integer, String> lruDemo = new LruDemo<>(3);
        lruDemo.put(1, "a");
        lruDemo.put(2, "b");
        lruDemo.put(3, "c");
        System.out.println(lruDemo.keySet());   // [1, 2, 3]

        // 访问 1
        System.out.println(lruDemo.get(1));
        System.out.println(lruDemo.keySet());   // [2, 3, 1]

		// 添加 4,最旧的 2 被删除
        lruDemo.put(4, "d");
        System.out.println(lruDemo.keySet());   // [3, 1, 4]
    }
}
3.2 自定义实现

分析:每次访问需要调整顺序,使用链表比较合适,增删比较快。根据 key 快速定位到元素位置,可以利用 HashMap 维护 Key 和 Node 节点。先通过 HashMap 获取对应的节点,之后再进行操作。

在这里插入图片描述

在本例中,head 是最新的(每次都加到 head.prev 处),tail 是旧的,删除先删 tail.next 遍历 keySet 索引越靠近 0 越新(与 LinkedHashMap 反方向实现的)

public class LruCacheDemo2<K, V> {
    private int cacheSize;	// 缓存大小
    private Map<K, Node<K, V>> nodeMap;	// node节点 Map 方便通过 key 快速访问
    // 维护链表,通过将 tail head 维护为一个虚拟节点,降低代码复杂度,不用担心 tail head 空指针的问题
    private NodeLinkedList<K, V> nodeLinkedList;

    public LruCacheDemo2(int cacheSize) {
        this.cacheSize = cacheSize;
        this.nodeMap = new HashMap<>(cacheSize);
        this.nodeLinkedList = new NodeLinkedList<>();
    }

	// 节点对象
    static class Node<K, V> {
        K key;
        V value;

        public Node() {
        }

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

		// 前后指针
        Node<K, V> prev;
        Node<K, V> next;
    }

    static class NodeLinkedList<K, V> {
        Node<K, V> tail;
        Node<K, V> head;

        // 头尾指针都是虚拟指针,这样就可以避免删除的时候移动 头尾指针 了
        public NodeLinkedList() {
            this.tail = new Node<>();
            this.head = new Node<>();

            head.prev = tail;
            tail.next = head;
        }

        protected void remove(Node<K, V> node) {
            if (node == null) {
                throw new IllegalArgumentException("node is null");
            }

            // 因为 tail head 都是虚拟节点,删除直接删除即可,不用考虑 空指针 的情况
            node.prev.next = node.next;
            node.next.prev = node.prev;
            // 删除节点:前后指针清空指向
			node.prev = null;
			node.next = null;
        }

		// 添加头结点
        protected void addHead(Node<K, V> node) {
            if (node == null) {
                throw new IllegalArgumentException("node is null");
            }
			
			// 节点添加,看上图方便理解
			// node 节点前后指向
            node.prev = this.head.prev;
            node.next = this.head;
            // 维护原来节点的指向,将 node 添加
            this.head.prev.next = node;
            this.head.prev = node;
        }
    }

    public V get(K key) {
        Node<K, V> node = nodeMap.get(key);
        if (node == null) {
            return null;
        }

        // 存在,就需要将当前节点放到第一个(head.prev)处
        // 先删除,后添加到头(比直接修改方便理解)
        nodeLinkedList.remove(node);
        nodeLinkedList.addHead(node);

        return node.value;
    }

    public void put(K key, V value) {
    	// 存在只需要更新和换位
        if (nodeMap.containsKey(key)) {
            Node<K, V> node = nodeMap.get(key);
            node.value = value;

            // 更新数据之后维护访问顺序
            nodeLinkedList.remove(node);
            nodeLinkedList.addHead(node);
        } else {
            // 新节点,判断下是否放满了
            if (nodeMap.size() == cacheSize) {
                Node<K, V> last = nodeLinkedList.tail.next;
                // Map 根 链表同步删除(删除 last)
                nodeLinkedList.remove(last);
                nodeMap.remove(last.key);
            }
        }

        Node<K, V> node = new Node<>(key, value);
        nodeMap.put(key, node);
        nodeLinkedList.addHead(node);
    }

    public boolean remove(K key) {
        Node<K, V> node = nodeMap.get(key);
        if (node == null) {
            throw new IllegalArgumentException("key not exists");
        }

        nodeLinkedList.remove(node);
        nodeMap.remove(key);
        return true;
    }

	// 模仿 LinkedHashMap 迭代器的遍历方法
    public String ketSet() {
        if (this.nodeMap.size() == 0) {
            return "[]";
        }

        StringBuilder builder = new StringBuilder();
        builder.append("[");
        Node<K, V> tmp = this.nodeLinkedList.head.prev;
        while (true) {
            builder.append(tmp.key);
            tmp = tmp.prev;
            if (tmp == this.nodeLinkedList.tail) {
               break;
            }
            builder.append(", ");
        }

        return builder.append("]").toString();
    }

	// 验证:索引越靠近 0 越新(与 LinkedHashMap 反方向实现的)
    public static void main(String[] args) {
        LruCacheDemo2<Integer, String> lruCache = new LruCacheDemo2<>(3);

        lruCache.put(1, "A");
        lruCache.put(2, "B");
        lruCache.put(3, "C");
        System.out.println(lruCache.ketSet());  // [3, 2, 1]

        System.out.println(lruCache.get(2));
        System.out.println(lruCache.ketSet());  // [2, 3, 1]

        System.out.println(lruCache.get(1));
        System.out.println(lruCache.ketSet());  // [1, 2, 3]

        lruCache.put(4, "D");
        System.out.println(lruCache.ketSet());  // [4, 1, 2]

        lruCache.remove(4);
        lruCache.remove(2);
    }
}

感兴趣的小伙伴可以 Debug 一下,应该会有更深刻的理解。

4. 写到最后

LRU 第二个方法参考的:尚硅谷Java大厂面试题第3季,周阳老师主讲的 P65-P70 感兴趣的小伙伴欢迎前去学习。

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值