LRU算法介绍和在JAVA的实现及源码分析
一、写随笔的原因:最近准备去朋友公司面试,他说让我看一下LRU算法,就此整理一下,方便以后的复习。
二、具体的内容:
1.简介:
LRU是Least Recently Used的缩写,即最近最少使用。算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小”。
这里不得不说i一下LFU(Least Frequently Used),其核心思想是“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”
LRU和LFU算法的区别是概括来说,LRU强调的是访问时间,而LFU则强调的是访问次数。
2.使用场景:
一般来说它是用来作为一种缓存淘汰算法。
3.实现方法:
方法一:用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。该方法有个致命的缺点就是需要不停地维护数据项的访问时间戳,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。
方法二:可以使用链表来实现,分三步走:首先当新数据插入的时候,依次按顺序放到链表中; 其次查询数据时,已经存在链表中时(即缓存数据被访问),则将数据移到链表尾部;当链表满的时候,将链表顶部的数据丢弃。
4.Java中的实现代码:
Java中最简单的LRU算法实现,就是利用LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可,内部就是使用的方法二实现的。代码如下:
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUTest {
static class LRULinkedHashMap<K,V> extends LinkedHashMap<K,V> {
//定义缓存的容量
private int capacity;
//带参数的构造器
LRULinkedHashMap(int capacity){
//如果accessOrder为true的话,则会把访问过的元素放在链表后面,放置顺序是访问的顺序(LinkedHashMap里面的get方法,当accessOrder为true,会走afterNodeAccess方法将节点移到最后)
//如果accessOrder为flase的话,则按插入顺序来遍历
super(16,0.75f,true);
//传入指定的缓存最大容量
this.capacity=capacity;
}
//实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素
@Override
public boolean removeEldestEntry(Map.Entry<K, V> eldest){
return size()>capacity;
}
}
//test
public static void main(String[] args) {
LRULinkedHashMap<String, Integer> testCache = new LRULinkedHashMap<>(3);
testCache.put("A", 1);
testCache.put("B", 2);
testCache.put("C", 3);
System.out.println(testCache.get("B"));
System.out.println(testCache.get("A"));
testCache.put("D", 4);
System.out.println(testCache.get("D"));
System.out.println(testCache.get("C"));
}
}
输出结果:
为何上述简单的代码LRU算法,具体要看LinkedHashMap里的put和get方法的源代码。
1.首先是put()方法,这个在LinkedHashMap并没有重写,直接使用的是HashMap中的put()方法,这里解释一下上面的重写的removeEldestEntry()方法,在上次HashMap原理探究中,put()方法会调用内部的putVal()方法,putVal()方法中最后会调用一个afterNodeInsertion(evict);这个方法在LinkedHashMap里面有实现:
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; //头结点 if (evict && (first = head) != null && removeEldestEntry(first)) { //会调用我们重写的removeEldestEntry() K key = first.key; removeNode(hash(key), key, null, false, true); // 满足条件,这移除头结点 } }
会调用我们重写的removeEldestEntry()方法,当返回为true时,则会removeNode()方法移除链表的顶端元素,满足方法二中的第三个步骤。
2.接下来看一下get()方法,这个在LinkedHashMap有重写,代码如下:
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; }
这里会用到我们初始化时设置的accessOrder的值,我们当时设置的是ture,也就意味着会调用afterNodeAccess()方法,我们继续看afterNodeAccess的源码:
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; 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; } }
源码中有块英文注释// move node to last,很明显是将查到的数据移动到链表的尾部。满足方法二中的第二个步骤。
至于方法二中的第一个步骤,LinkedHashMap本身就是有顺序的插入,顺带分析下为啥它是如何实现有顺序插入的。由于它并没有单独实现put()方法,所以要看HashMap中的put()实现,还是参照上次HashMap原理探究 ,在put()方法会调用内部的putVal()方法,putVal()方法中添加新节点会调用newNode()方法,而LinkedHashMap对newNode进行了重写,代码如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); // 这个方法就是将新节点顺序的插入到为节点的后面 return p; }
linkNodeLast()方法:
// link at the end of list 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; } }
三、总结:
本篇随笔主要就是LRU的介绍和在java中的快速实现,实现的代码相信网上非常的多,主要还是从源码角度分析了为何可以这样简单的实现,让我们能够更加理解LRU的基于链表的实现吧,同时也分析了下LinkedHashMap的部分源码实现,让大家能够更深入的理解吧。