LRU全称是Least Recently Used,即最近最久未使用的意思。
LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
解决的实际问题:当做数据缓存时,缓存的数据会随着时间的推移越来越多,如果没有缓存清除策略,那么会出现俩个问题:1、缓存越来越大挤爆内存。2、很多不使用的数据占据这内存空间,导致内存得不到有效利用。
此场景使用LRU算法非常合适。
LRU算法的主要思想:1.设置一个缓存阈值,超过阈值删除最老的数据。
2.保证最老的数据总是在链表的头部,最新的数据总是在尾部,这样每次需要删除数据时把头部数据删除即可。
linkedHashMap对LRU算法的实现:
1.HashMap中的三个没实现的方法,在linkedHashMap实现:
//把新插入的元素放到链表尾部
void afterNodeAccess(Node<K,V> p) { }
//在放入新元素后,检查是否要删除最老的元素,需要则删除
void afterNodeInsertion(boolean evict) { }
//在删除元素之后调整头尾部元素
void afterNodeRemoval(Node<K,V> p) { }
2.实现思路:
1.在put方法时:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
如果新加的是已经存在于链表中的,那么把元素调整到链表尾部,以保证最近数据都在尾部。
afterNodeInsertion(evict);
如果新增数据不是旧数据,那么调用afterNodeInsertion方法,检查是否要删除最老的元素,需要则删除。afterNodeInsertion在linkedHashMap中的实现:
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);
}
}
可以看出,删除老数据的条件中最重要的是removeEldestEntry方法的返回值为true。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
此方法默认的返回值是false,说明linkedHashMap默认是不会删除老数据的,真正要实现LRU算法得自己重写此方法。
如果满足删除最老数据的条件,那么会调用removeNode方法:
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
可以看到在删除数据之后会调用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;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
根据删除节点的前后节点,重新确定链表的顺序,以保证链表的连续性。
总结:在链表中新增数据时,首先判断数据是否是老数据,是的话把数据调整到链表尾部,以保证最新数据一直在尾部。不是的话在数据加入之后,判断是否需要删除最老的数据,需要则删除。
通过前面的分析,要真正实现LRU算法需要重写removeEldestEntry方法。在druid 的jar包中已有了相关实现:
package com.alibaba.druid.util;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 1L;
private final int maxSize;
public LRUCache(int maxSize){
this(maxSize, 16, 0.75f, false);
}
public LRUCache(int maxSize, int initialCapacity, float loadFactor, boolean accessOrder){
super(initialCapacity, loadFactor, accessOrder);
this.maxSize = maxSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return this.size() > this.maxSize;
}
}
而且redis也对LRU算法有相关的支持,修改redis.conf:
# maxmemory <bytes>
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
maxmemory 设置最大存储量,超过存储量后使用相关策略进行回收。