LinkedHashMap实现LRU原理解析

LRU介绍

LRU是Least Recently Used 最近最少使用算法。是一种常用的内存管理的页面置换算法。
计算机中用缓存来存放以前读取的数据,而不是直接丢掉,这样,再次读取的时候,可以直接在缓存里面取,而不用再重新查找一遍,这样系统的反应能力会有很大提高。但是,当我们读取的个数特别大的时候,我们不可能把所有已经读取的数据都放在缓存里,毕竟内存大小是一定的,我们一般把最近常读取的放在缓存里。
LRU就是实现了这种思想,也就是在缓存中把最近未使用的向后移让位与最新进入的数据。

我们可以简单的用一个链表实现这样一种思想。因为访问的元素就要移动到表头,用链表实现插入移出只用O(1)时间,而数组移动就需要O(N)的时间,所以用链表实现最简单且最高效。当读取一个数据时就将其加入到队列头部,如果是从队列中读取数据,还要把数据从元位置删除并将其加入头部。这样当缓存不够时就从队尾删除数据。这样就可以简单的实现LRU思想。

LinkedHashMap简述

LinkedHashMap实现了两种元素的排序方式:插入排序和访问排序。
这个访问排序不就是实现了上述的LRU思想吗?
LinkedHashMap继承自HashMap,可能各位要想,HashMap是一个Map,而上述我们要实现的明显是一个链表。Map如何有链表的功能呢。接下来我们来看看LinkedHashMap的具体实现

源码解读

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    Entry<K,V> before, after;
    ......
}
private final boolean accessOrder;
private transient Entry<K,V> header;
void init() {
    header = new Entry<K,V>(-1, null, null, null);
    header.before = header.after = header;
}

这时LinkedHashMap的两个属性,从这里可以看到要建链表的迹象啊。还有数据排序方式,不过这个排序方式默认是插入模式。
LinkedHashMap的节点继承自HashMap.Entry,不过新加入了before和after属性,这就使得我们的排序链表中前后的元素就能关联起来。而HashMap.Entry中也有next属性,这是HashMap处理hash碰撞后所用到的下一个元素的引用。

《LinkedHashMap实现LRU原理解析》

这时HashMap的示例图,图中的小箭头就是next。但是你能想想不在同一行的节点也能通过after和before连在一起吗?这个链表就是我们的LinkedHashMap的链表。

LinkedHashMap的存取

LinkedHashMap并未实现put方法,而是实现put()需要用到的addEntry()方法。

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                //如果put的元素Map中本来就有,就要按照排序方式进行调整。
                e.recordAccess(this);//此方法是在LinkedHashMap中实现
                return oldValue;
            }
        }
        modCount++;
      //如果put的元素要新加入到map中,就要用子类的方法
        addEntry(hash, key, value, i);//LinkedHashMap重写这个方法
        return null;
    }

上述方法还是要将节点加入到上图所示的HashMap中的数组中。只不过各个节点按照LinkedHashMap的排序方式,维护了before和after的属性。

下面我们来看一看如果put的元素已经存在于map中,即访问map中原有的元素,LinkedHashMap如果实现调整。

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    if (lm.accessOrder) {
    //accessOrder默认是false,即按插入顺序。如果是访问顺序就要进行下面操作
        lm.modCount++;
        remove();//在原队列中删除当前节点
        addBefore(lm.header);//移动当前节点
    }
}
private void remove() {
//修改前后节点的after和before引用,将当前节点从当前位置去除
    before.after = after;
    after.before = before;
 }
private void addBefore(Entry<K,V> existingEntry) {
//添加到队首,双向队列,哪来的队首??,哈哈
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}

凭借上面的三个方法,就可以将LinkedHashMap维护的队列进行处理,如果accessOrder是插入顺序,就不做任何处理。如果按照访问顺序,按照LRU原则,访问的元素要放在最前面,所以要更改当前节点的after和before引用以达到更改位置的目的。

如果put的元素map中没有,put循环结束就进入到addEntry()方法。

void addEntry(int hash, K key, V value, int bucketIndex) {
       createEntry(hash, key, value, bucketIndex);
       Entry<K,V> eldest = header.after;//队尾元素
       if (removeEldestEntry(eldest)) {//永远返回false,即不删除链表元素
           removeEntryForKey(eldest.key);
       } else {
           if (size >= threshold)
               resize(2 * table.length);
       }
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
     table[bucketIndex] = e;
     e.addBefore(header);
     size++;
}

HashMap中的addEntry()方法

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
    }

两个类将新的元素加入到map的方法还是有很多相似的地方,毕竟都是加入HashMap维护的数组中。不同的是,LinkedHashMap要维护自己的队列,要维护自己队列中的after和before属性。

LinkedHashMap中的removeEldestEntry()方法永远返回false,这样做的本意为链表是没有size限制的,如果我们要做LRU,就可以重写这个方法让其返回true。

而LinkedHashMap中的get()方法只是比HashMap中多了一个维护自己链表的工作而已

 public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);//调用HashMap的方法
        if (e == null)
            return null;
        e.recordAccess(this);//维护自己的队列
        return e.value;
    }

总结

LinkedHashMap继承自HashMap,但是一个Map无论如何也是实现不了链表的功能。所以LinkedHashMap自己维护了一个双向链表。这个双向链表按照我们想要的顺序把HashMap中的元素串联起来实现链表。也就是说,数据的存储还是按照HashMap的方式存放,但是LinkedHashMap中维护了一个链表将所有的元素串联起来。

    原文作者:徐士林
    原文地址: https://www.jianshu.com/p/1a66529e1a2e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞