LRU
环境:JDK11
最近接触LRU(Least Recently Used),即最近最少使用,也称淘汰算法,在JDK中LinkedHashMap
有相关实现
LRU的LinkedHashMap实现
LinkedHashMap
继承HashMap
。所以内存的存储结构和HashMap
一样,但是LinkedHashMap
比HashMap
多一个head
和tail
,这是一个双向链表,head
表示最早添加进Map中的元素,也就是最老的数据,tail
就是最后加进来的元素,当满足某种条件后,就会自动删除head
节点。LRU就是这样的。
transient LinkedHashMap.Entry<K,V> head;transient LinkedHashMap.Entry<K,V> tail;
添加操作
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
put
方法中会创建一个Node节点,LinkedHashMap
重写了newNode
方法,创建的node是LinkedHashMap.Entry
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {LinkedHashMap.Entry<K,V> p =new LinkedHashMap.Entry<>(hash, key, value, e);// 使用head和tail组成链表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;}
}
put
方法结束后还会调用afterNodeInsertion
方法,一个空实现的方法,该方法在LinkedHashMap
中实现了
//evict: 是否在插入后进行驱逐操作,比如LinkedHashMap实现颗LRU算法,会把老的数据删除
void afterNodeInsertion(boolean evict) { }
void afterNodeInsertion(boolean evict) { // possibly remove eldestLinkedHashMap.Entry<K,V> first;if (evict && (first = head) != null && removeEldestEntry(first)) {// 删除最老的数据K key = first.key;removeNode(hash(key), key, null, false, true);}
}
LinkedHashMap
中removeEldestEntry
方法默认返回false。removeEldestEntry
方法表示达到一定条件后,LinkedHashMap
就会自动删除最老的数据,比如当缓存容量达到一定值之后,需要把最不经常使用的缓存删掉或者保存到磁盘中。所以使用LinkedHashMap
来实现LRU算法,记得继承LinkedHashMap
重写removeEldestEntry
方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;
}
比如我们定义一个LRULinkedHashMap类,继承LinkedHashMap,重写removeEldestEntry方法,记得把accessOrder设置为true。
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {private int threshold;public LRULinkedHashMap(int threshold) {super(16, 0.75f, true);this.threshold = threshold;}@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {return size() > threshold;}
}
取出操作
accessOrder
为true,取出操作,就会影响节点在head和tail组成的链表中的位置,即访问次数越多,数据越活跃。
accessOrder为false,就跟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;
}
afterNodeAccess
方法就是将最活跃的节点放到tail
void afterNodeAccess(Node<K,V> e) { // move node to last// 最新的数据,或者叫最活跃的数据LinkedHashMap.Entry<K,V> last;// 将节点e变成tail,tail表示目前最活跃的数据节点// 如果e已经是tail了,即已经是最活跃的节点了,就不要处理了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;elseb.after = a;if (a != null)a.before = b;elselast = b;if (last == null)head = p;else {p.before = last;last.after = p;}tail = p;++modCount;}
}
删除操作
使用remove
方法删除节点后,会调用afterNodeRemoval
方法,HashMap
对此方法是空实现,LinkedHashMap
重写了这个方法,就是将e
从链表中删除
void afterNodeRemoval(Node<K,V> e) { // unlinkLinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;p.before = p.after = null;if (b == null)head = a;elseb.after = a;if (a == null)tail = b;elsea.before = b;
}
遍历LinkedHashMap
这是题外话了,来都来了,就讲讲吧
HashMap
在遍历的时候,是乱序的,即不是按照添加的顺序遍历。LinkedHashMap
可以顺序遍历,我们看看怎么实现的。
map.forEach((k, v) -> {System.out.print(k + ":" + v + " ");
});
public void forEach(BiConsumer<? super K, ? super V> action) {if (action == null)throw new NullPointerException();int mc = modCount;// 实际是在遍历这个链表for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)action.accept(e.key, e.value);if (modCount != mc)// 遍历的过程中不能修改LinkedHashMapthrow new ConcurrentModificationException();
}
可以使用iterator遍历,中途可以删除节点iterator.remove();
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();while (iterator.hasNext()){Map.Entry<String, Integer> next = iterator.next();String key = next.getKey();Integer value = next.getValue();// 使用iterator的remove方法删除节点没事iterator.remove();System.out.print(key + ":" + value+ " ");
}