LRU与LinkedHashMap的不解情缘

1.前言

最近在学习Mybatis源码关于一级和二级缓存的过程中,有这么一个类,LruCache.class。按照设计模式来说这里用到了装饰者的设计模式,维护一个接口类【Cache delegate;】的变量。当然这不是重点,重点是他有一个成员变量,keyMap,实例化对象是LinkedHashMap。更要紧的是,他在创建这个对象的时候,还重写了removeEldestEntry这个方法(如下代码),那么LRU是个什么鬼,这个算法与LinkedHashMap又有什么关系,让我们走进LinkedHashMap的源码,看看究竟是一个说明鬼?

//org.apache.ibatis.cache.decorators.LruCache
public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

2. LRU概述

在进入LinkedHashMap源码之前,我们有必要先了解一下什么是LRU。LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。(允许我懒一把,摘自百度百科).。用一句话概括,为了减少性能消耗,LRU会将最旧的元素移除,从而保证缓存总个数恒定。那么就需要解决两个问题:1)如何判断哪个元素最旧;2)何时进行移除最旧元素。带着这些问题,我们就踏进LinkedHashMap的世界吧。

3 LinkedHashMap

3.1 概述

根据JDK官方提供的说明文档,我们知道这么几件事:

1)它维护了两个变量,head和tail,采用双向链表的形式展现;

2)他提供了一个特殊的构造函数{@link #LinkedHashMap(int,float,boolean)},其迭代顺序是其条目的最近访问顺序(访问顺序),这种映射非常适合构建LRU缓存。可以重写{@link #removeEldestEntry(Map.Entry)}方法,以强加一个策略,以便在将新映射添加到map集合时自动删除陈旧的映射。

好了,这两点是我们最为关注的。

3.2 HashMap为LinkedHashMap做了哪些准备

翻开HashMap的源码,我们会发现有三个特殊的方法。

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

JDK给出的解释是允许LinkedHashMap后置回调。那么这几个方法都在什么地方出现了,我们通篇查找了一下,在HashMap中,被调用的方法包括:

1.afterNodeAccess
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
public boolean replace(K key, V oldValue, V newValue)
public V replace(K key, V value)
public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction)
public V computeIfPresent(K key,BiFunction<? super K,? super V,? extends V> remapping)
public V get(Object key)[from LinkedHashMap]
public V getOrDefault(Object key, V defaultValue)[from LinkedHashMap]

2.afterNodeInsertion
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remapping)
public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remapping)

3.afterNodeRemoval
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)

这里基本囊括了HashMap的增删改查所有的方法,不过需要指出的是HashMap的get相关的方法并没有调用afterNodeAccess,而是交给了LinkedHashMap调用。那么这三个方法都做了什么事?让我逐个看一下。

3.3 afterNodeAccess

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        //如果访问的元素不是尾部元素,则执行如下操作:即将当前访问的元素放置到最后一位。
        //如果是尾部元素,则不执行任何操作
        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;
            //将当前访问节点追加到最后一位
            //如果last为空,则p还是头部节点
            if (last == null)
                head = p;
            else {
                //双向链表追加到尾部
                p.before = last;
                last.after = p;
            }
            //此时更新尾部节点为p
            tail = p;
            ++modCount;
        }
    }

这里有一句关键的代码:if (accessOrder && (last = tail) != e) 。对于accessOrder,jdk给出的解释是:

The iteration ordering method for this linked hash map: true for access-order,false for insertion-order.
用于linkedHashMap的迭代排序方法:true->访问排序;false:插入排序

而(last = tail) != e判断是否是尾部元素,如果是那就不用那么麻烦了,否则将此元素放到链表的尾部,同时维护链表的双向性。具体代码大家可以自行分析一下。写到这么,大家明白LinkedHashMap为什么要用双向链表来存储数据了吧。

3.4 afterNodeRemoval

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    //前置节点为null,则之前e为头结点,现在将e移除,则e的后置节点b为头结点
    if (b == null)
        head = a;
    else
        //将e的前置节点的后置节点设置为e的后置节点
        b.after = a;
        //双向链表设置同理
        if (a == null)
            tail = b;
        else
            a.before = b;
}

这段代码很好理解,就是将当前节点移除,同时维护链表的双向性。

3.5 afterNodeInsertion

这个方法是这三个方法中最重要的一个方法。前面提到他可以作为LRU的数据结构,那么为了保证缓存数据个数的稳定性,就需要将最旧使用的元素移除掉。我们先看一下代码:

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

咦,虽然这个方法对于LRU起到关键性的作用,但是仿佛并没有那么复杂。无非就是进行一系列判断,然后决定是否移除元素。这个最旧的元素是怎么确定的?通过上面的两个方法我们知道HashMap经过增删改查后,都会将当前元素放到链表的最后。那么我们就可以认为第一个元素就是最旧的元素。找到最旧的元素后,那么就是如何判断最旧的元素是否应该被删除。,通过代码我们很容易知道这项任务交给了判断语句上,if (evict && (first = head) != null && removeEldestEntry(first));这里有三个条件判断,只有这三个条件全部满足,才可以删除最旧的元素。

1)evict。除了putVal之外,其余传递的都是true。putVal中也即是在clone方法和以一个就的Map初始化新的HashMap的时候evict才为false,也即是这两个操作不会移除Map中的元素。

2)(first = head) != null。意思是头部元素不为空的情况下为true。也就是说,如果头部元素为空,意味着这个链表为空链表,也就不需要移除元素。

3)removeEldestEntry(first)。这个方法才是这个类的灵魂所在。默认实现是返回false,也就是默认不会删除最旧元素。当要构建一个LRU算法的过程中,必须重写这个方法,如果当前元素个数超过指定的个数,这样就返回true。一旦返回true,那最旧的元素就会被删除。

4. 总结

分析到这里,我们终于明白LinkedHashMap是怎么实现LRU的,他的一个关键方法便是removeEldestEntry()。回到文章开始,我们再来看一下那段代码。

//org.apache.ibatis.cache.decorators.LruCache
public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

当LinkedHashMap中的元素个数大于指定的size的时候,就要返回true。对于eldestKey = eldest.getKey();那是要从PerpetualCache中移除最旧元素。对于PerpetualCache,有兴趣的可以自行看一下。

5 题外话

Mybatis中真正的二级缓存其实是PerpetualCache,而LRUCache只不过是他的一种回收策略。

发布了72 篇原创文章 · 获赞 24 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/oYinHeZhiGuang/article/details/103842043