LRU,全称Least Recently Used,即最近最久未使用算法,用于操做系统的页面置换算法,以及一些常见的框架。其原理实质就是当须要淘汰数据时,会选择那些最近没有使用过的数据进行淘汰,换句话说,当某数据被访问时,就把其移动到淘汰队列的队首(也就是最不会被淘汰的位置)java
基于这样的原则,咱们就能够着手实现了。不过Java已经为咱们提供了一个现成的模板,咱们站在巨人的肩膀上,能够参考一下Java是如何实现LRU功能的算法
在LinkedHashMap中,有一个不多用到的构造函数:缓存
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
复制代码
其中accessOrder
这一属性,在其余的构造函数中是默认为false
的,若是咱们经过该构造函数将其设为true
以后,就实现了LRU功能,下面的程序简单了作了下演示:app
public static void main(String[] args) {
int cacheSize = 3;
// 最大容量 = (缓存大小 / 负载因子)+ 1,保证不会触发自动扩容
LinkedHashMap<String, String> cache = new LinkedHashMap<String, String>(
(int)(cacheSize/ 0.75f) + 1, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > cacheSize;
}
};
cache.put("1", "a");
cache.put("2", "b");
cache.put("3", "c");
// head => "1" => "2" => "3" => null
// put已存在的值,和get方法是同样的效果
cache.put("1", "a");
// head => "2" => "3" => "1" => null;
cache.put("4", "d");
// head => "3" => "1" => "4" => null;
for (String key: cache.keySet()) {
System.out.println(key);
}
}
复制代码
其实还有很重要的一点,就是须要重写removeEldestEntry()
这一方法,默认是返回false
的,当返回true
时,会移除最久没有使用的节点,因此咱们要作的,就是当容量达到缓存限制时,移除LRU算法断定的最近最久未使用节点框架
能够看到,咱们依次插入节点一、二、3后,若是此时再插入节点4,就会致使removeEldestEntry()
返回为true
,而后移除队首节点,即节点1。可是咱们这里因为中间重复插入了一次节点1,因此会判断节点1是“常常访问的节点”,因此节点1被提到链表最后,队首节点就变成了节点2,当容量超过限制时,会把节点2移除ide
探索LinkedHashMap中LRU的实现原理,咱们就要追溯到HashMap中的putVal
方法,这个方法最后触发了一个回调函数:函数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// ...
if (e != null) { // existing mapping for key
// ...
afterNodeAccess(e);
return oldValue;
}
}
afterNodeInsertion(evict);
return null;
}
复制代码
putVal()
方法在插入后会触发方法的回调,有两种状况:this
afterNodeAccess(e)
afterNodeInsertion(evict)
其中,变量e是“撞车”的节点,变量evict在子类不重写put()
方法的状况下是默认为true
的,因此咱们就把它看成常量来看spa
而后咱们回到,LinkedHashMap中,来看这个两个钩子方法(HashMap中这两个方法实现均为空):操作系统
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
// 如下状况知足时,调用removeNode移除最久未使用的节点:
// 1. evict为true
// 2. 头结点不为空
// 3. 符合移除条件:removeEldestEntry返回true
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// 开启LRU模式,且访问的节点不是尾节点,则将被访问的节点置于链表尾
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;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
复制代码
显而易见,afterNodeInsertion
负责在插入以后判断是否须要移除最近最久未使用的节点(即链表头节点),afterNodeAccess
负责在访问某节点以后,将该节点移动到链表尾
在afterNodeAccess
中,由于要考虑到各类特殊状况,并且是一个带有头尾节点的双向链表,因此状况判断比较复杂,实际上就是将指定节点移动到队尾,若是本身想实现一个相似的功能能够不作的这么复杂
通常来讲,若是想作一个LRU算法实现的话,LinkedHashMap就能知足须要了。要是想本身实现的话,这里提供一个实现的思路: