LRU容器设计

501 阅读5分钟

LRU内部可以使用LinkedHashMap实现。

HashMap

HashMap采用数组+单链表实现


LinkedHashMap

LinkedHashMap继承自HashMap,是一种有序的Map。

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

HashMap+LinkedList(双向链表),在原有HashMap数据结构基础上,其构造方式跟HashMap完全一致,在HashMap 的节点的基础上, 加上了两个引用来将所有entry节点串联成一个双向链表。这个顺序默认是数据插入顺序。在构造方法里accessOrder = true,表示按访问顺序。

  /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;
    //指定遍历LinkedHashMap的顺序,true表示按照访问顺序,false表示按照插入顺序,默认为false
    final boolean accessOrder;

LinkedHashMap中没有重写父类的Put方法,因此使用HashMap中的Put方法。但是重写了构建节点方法newNode()。

HashMap Put(k,v)方法代码如下,final关键字表示Put方法是不可以被重写的,可知所有继承自HashMap的类都是直接使用其父类的Put方法。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
      //如果是空桶,执行初始化,并且扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
//如果没有Hash碰撞,桶位置没有value,就新建一个Node,放在该bucket位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
//如果Hash值相等,key也相等,直接替换value值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)//该位置节点是红黑树节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
          //发生hash碰撞,在该bucket节点后面插入一个普通节点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {//遍历到尾部
                    p.next = newNode(hash, key, value, null);//新建一个节点放到尾部
                    //如果追加节点后,链表数量》=8,则转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
//如果找到了要覆盖的节点,则p.next在上面已经指向了新加入的节点,并且新加入的节点得到了老节点的引用,只需要替换value值即可。
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;//p指向下一个节点,循环往后找
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;//这时候e拿到的是要替换的节点引用,所以里面的value值是老值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;//替换新值
            afterNodeAccess(e);
            return oldValue;
        }
    }
//如果执行到这里,说明没找到相同的key,是插入新节点,modeCount+1,注:modeCount表示修改次数,为了保证线程安全
    ++modCount;
//判断是否需要扩容
    if (++size > threshold)
        resize();
//空实现,LinkedHashMap重写使用
    afterNodeInsertion(evict);
    return null;
}

在插入的时候LinkedHashMap复写了HashMap的newNode方法,并且在方法内部更新了双向链表的指向关系。

LinkedHashMap中重写newNode方法

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//内部使用父类HashMap的构造方法
    LinkedHashMapEntry<K,V> p =
        new LinkedHashMapEntry<K,V>(hash, key, value, e);
    linkNodeLast(p);//把节点添加到双链表的尾部
    return p;
}

private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
//如果是空map,则head= tail = p
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;//tail永远等于最新的节点,有最新节点的引用
    if (last == null)
        head = p;//head只在空表插入节点时等于最老的节点,因此head永远是最老的
    else {
      //构建双链表关系
        p.before = last;
        last.after = p;
    }
}

HashMap在插入的时候调用了afterNodeInsertion()方法,在HashMap中这个方法是空实现,而在LinkedHashMap中则有具体实现

//是否要移除最老的节点(即Head节点),LRU功能会用到。所以LRU可以使用LinkedHashMap来实现。evict在Put(K,V)中默认为true
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
//
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

//默认不移除
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

如果我们要根据LinkedHashMap 实现一个LruCashed , 我们只需要继承LinkedHashMap ,重写removeEldestEntry, 当当前长度> 缓存长度, 返回true 即可。

LinkedHashMap的get(K key)方法中调用了afterNodeAccess方法。

//将访问过的节点移到双链表队尾,并将引用赋值给tail
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMapEntry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<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;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

afterNodeAccess()方法中如果accessOrder=true时会移动节点到双向链表尾部。这就是为什么访问顺序输出时访问到的元素移动到链表尾部的原因。



LRU实现

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

capacity:桶的数量,即桶数组的size

capacity译为容量。capacity就是指HashMap中桶的数量。默认值为16。一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂

loadFactor

loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。

threshold

threshold表示当HashMap的size大于threshold时会执行resize操作。
threshold=capacity*loadFactor

自定义LruCache:

public class LruCache extends LinkedHashMap<Integer, Integer> {
	
	private int cacheSize;
	
	public LruCache(int cacheSize){
		super(0,0.75f,true);
		this.cacheSize = cacheSize;
	}

	public static void main(String[] args) {
		LruCache lruCache = new LruCache(3);
		lruCache.put(1, 1);
		lruCache.put(2, 2);
		lruCache.put(3, 3);
		lruCache.put(4, 4);
		lruCache.put(5, 5);
		for(Map.Entry<Integer, Integer> entry: lruCache.entrySet()){
			System.out.println(entry.getKey());
		}
	}

	@Override
	protected boolean removeEldestEntry(java.util.Map.Entry<Integer, Integer> eldest) {
		// TODO Auto-generated method stub
		return size()>cacheSize;
	}	
}

输出结果为:

3
4
5