LRU
LRU是Least Recently Used的缩写,即最近最少使用。
----摘自百度百科
LRU最近最少使用,说的直白一点就是最久未使用。我们可以利用这一特点来做缓存的淘汰策略再合适不过了,当缓存满了的时候使用这种算法进行数据淘汰。
在Java里面LinkedHashMap自己实现了LRU,使用的数据结构是HashMap+双向链表,由于LinkedHashMap继承自HashMap,只是增加了数据有序与LRU这一部分,其余方法都是继承自HashMap,所以本篇文章只针对LinkedHashMap中关于LRU的部分来进行解读,如要看HashMap,请看我的另一篇文章:《HashMap2》。我们先来看一看LinkedHashMap是如何实现LRU的:
LinkedHashMap的源码如下:
LinkedHashMap中的一个内部类(实现双向链表):
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);
}
}
以下是LinkedHashMap重要的三个属性:
/**
* 双向链表的头节点
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 双向链表的尾节点
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
这个属性是控制LinkedHashMap中元素的顺序,是根据插入顺序排序,还是根据访问顺序排序 false代表按插入顺序 ,true代表按照访问顺序,此属性是能够实现LRU的关键
*/
final boolean accessOrder;
LinkedHashMap的默认元素排序顺序:
public LinkedHashMap() {
super();
//默认按照插入顺序排序
accessOrder = false;
}
我们先来看get流程:
public V get(Object key) {
Node<K,V> e;
//如果再LinkedHashMap中获取不到key对应的value,则返回空
if ((e = getNode(hash(key), key)) == null)
return null;
//这个判断很重要,前面说过了,LinkedHashMap是默认按照插入顺序有序的,这里则表示按照访问顺序有序,那么我们如果访问到了这个元素,则要将他放到链表的尾部,这里为什么是头部而不是尾部,因为链表插入都是插入尾部呀,相当于是把原来位置的元素删掉,然后在尾部插入该元素
if (accessOrder)
afterNodeAccess(e);
// 返回获取到的value
return e.value;
}
具体来看实现:
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;
//如果b为null 那么表明p为头节点
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;
}
}
LinkedHashMap的链表remove流程:
注:这里只是列出了维护链表的代码,具体的remove流程请看HashMap的removeNode()方法:
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;
}
LinkedHashMap正儿八经实现LRU的方法:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//判断是否会触发LRU淘汰机制
if (evict && (first = head) != null && removeEldestEntry(first)) {
//移除头节点
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//此方法默认返回false,就是说,LRU不会执行,这个方法其实就是LRU淘汰数据触发的条件
//我们自己通过继承LinkedHashMap的时候就是通过重写此方法来实现LRU的。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
总结一下上述的实现方式:
在我个人看来,要通过双向链表+HashMap实现LRU只有以下几个关键点:
- 构造HashMap+双向链表的数据结构。
- 在put的时候,不光要put进HashMap里面,而且还要将元素接到双向链表的末尾。
- get的时候要将该元素放到双向链表的末尾
- remove的时候要移除双向链表中的节点,即维护双向链表的关系。
有了以上思路,我们就可以自己实现简单的LRU算法:
下面我就以2种方式实现LRU:
- 继承LinkedHashMap:
public class LRUCache2<K,V> extends LinkedHashMap<K,V> {
//定义缓存的容量
private int capacity;
//默认的缓存大小值为100
private static final int initialCapacity=100;
//指定默认的缓存大小
public LRUCache2(){
this(initialCapacity);
}
public LRUCache2(int capacity) {
//这个值一定要设置为true 才是根据访问顺序排序
super(capacity,0.75f,true);
this.capacity=capacity;
}
public V putVal(K k,V v){
return put(k,v);
}
public V getVal(K k){
return get(k);
}
public V removeVal(K k){
return remove(k);
}
//LRU淘汰机制触发的条件 关键在于重写这个方法
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size()>capacity;
}
}
- 自己构造双向链表+Hashmap
public class LRUCache {
//双向链表节点
class Node {
Integer key;
Integer value;
Node next;
Node pre;
public Node() {
}
public Node(Integer key, Integer value) {
this.key = key;
this.value = value;
}
}
//用于存储映射
private HashMap<Integer, Node> map = new HashMap<>();
//元素的个数
private Integer size;
//缓存的大小 容量
//当size>capacity时 HashMap就需要淘汰数据了
private Integer capacity;
//双向链表的头尾节点
private Node head;
private Node tail;
public LRUCache(Integer capacity) {
this.capacity = capacity;
this.size = 0;
head = new Node();
tail = new Node();
//初始化链表 head<--->tail
//这2个节点只是辅助节点 没有数据
head.next = tail;
tail.pre = head;
}
//LinkedHashMap里面是将元素插入到双向链表的尾部,此处我是将元素插入到头部的
public int get(int key) {
Node node = map.get(key);
if (node == null) return -1;
//如果key存在 先通过Hash表定位 然后移到链表头部
removeToHead(node);
return node.value;
}
public void put(int key, int value) {
Node node = map.get(key);
//说明 key不存在 那么则创建一个新的节点
if (node == null) {
Node newNode = new Node(key, value);
map.put(key, newNode);
addToHead(newNode);
size++;
//如果缓存已满 则删除链表的尾节点
if (size > capacity) {
//链表中移除尾节点
Node node1 = removeTail();
//删除对应的Hash映射
map.remove(node1.key);
size--;
}
} else { //这个key存在 那么新的值覆盖旧的值 并放在链表的头部
node.value = value;
removeToHead(node);
}
}
//移除元素 先删除HashMap中的映射,再维护链表关系
public void remove(Object key){
Node node=map.remove(key);
deleteNode(node);
}
//将最近访问过的链表节点移动到头部(删除该访问的节点,然后再新建一个节点添加到头部)
public void removeToHead(Node node) {
deleteNode(node);
addToHead(node);
}
//添加新的节点到头部
public void addToHead(Node node) {
node.pre = head;
node.next = head.next;
head.next.pre = node;
head.next = node;
}
//删除末尾的节点(实际上是末尾的上一个节点,这个末尾节点没有实际数据) 缓存淘汰
public Node removeTail() {
//获取尾节点的前一个节点 并删除
Node node = tail.pre;
deleteNode(node);
return node;
}
//删除节点
public void deleteNode(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
}
附上LinkedHashMap的图解:作者实在懒得画,网上找的。
作者感想:
写这一篇LRU原因有以下几点,首先java中的LRU在LinkedHashMap中实现了的,我们通过LRU可以更好的理解LinkedHashMap的数据结构与工作原理;再者LRU本身是很重要的一个算法,通过这一篇文章,我们可以理解LRU的实现,还可以自己手写LRU;还有一个很重要的原因,上一次面试某家互联网企业,面试官让我手撕LRU,我说了大概思路,但是在细节上没有回答好,非非我只能卒!!!哎,菜是原罪!!!希望各位小伙伴们看了我的文章,能有一点收获吧。