「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。
1.什么是LRU 算法
LRU (Least Recently Used) 算法是据数据的历史访问记录来进行淘汰数据,其核心思想是 “如果数据最近被访问过,那么将来被访问的几率也更高 "。当工作区内存满了之后,如果要添加新的数据,要对老的数据进行删除,最近没有被使用过的数据将会优先被淘汰。
2. java 中已经实现的LRU 数据结构
在java 中已经帮我们实现了这样一个数据结构,那就是LinkedHashMap 。LinkedHashMap 可以实现按照插入顺序排列Key Value ,也可以按照访问顺序排序K V 。 今天就来探索其LRU 的实现方式。
3. 源码阅读LinkedHashMap
LinkedHashMap继承自HashMap,它的多种操作都是建立在HashMap操作的基础上的。同HashMap不同的是,LinkedHashMap维护了一个Entry的双向链表,保证了插入的Entry中的顺序。详细代码如下:
//定义双向链表
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);
}
}
private static final long serialVersionUID = 3801124242820219131L;
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
//决定是否使用LRU
final boolean accessOrder;
//有参构造。当传入初始大小,影响因子,true 时,那么开启LRU。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
LinkedHashMap 继承了HashMap,既然是map 那么必然离不开put 和get。LinkedHashMap 没有重写put方法,所以插入方法依然调用了HashMap 的实现,但是与之不同的是,LinkedHashMap 重写了HashMap 中的以下几个方法。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {}
void afterNodeAccess(Node<K,V> p) {}
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
3.1 put方法逻辑分析
LinkedHashMap put 方法的伪代码逻辑如下:
put(ket ,val){
//哈希运算之后,如果目标位置没有元素,那么需要新建node
if((table[i]&hash)==null){
newNode(){
doSomething() { //新建node }
linkNodeLast(){ //新节点默认是tail,并且和原tail 链接 }
}
}
}else{
}
//修改完成之后,启动淘汰策略
afterNodeInsertion(){
//判断是否开启了LRU
if (evict && (first = head) != null && removeEldestEntry(first)) {
removeNode(){ //开启了,将被访问的节点移动到尾部 }
}
}
当插入新建元素时,如果tail 为空,那么说明才初始化,将新节点赋予头尾节点。
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;
}
}
接下来是afterNodeAccess ,核心代码如下 ,功能是当插入一个元素之后,会将插入的元素移动到双端队列尾部节点
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//如果开启了LRU ,且当前元素不是tail,那么当前节点移动到双端队列尾部
if (accessOrder && (last = tail) != e) {
//定义三个节点p ,b,a 分别保存 e ,e的前一个元素和后一个元素
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//移除节点后续,方便挂在尾部节点之后
p.after = null;
/** 这段代码含义是 如果原有的是双端队列顺序 为 1 <=> 2 <=>3 <=>4 <=>5,其中head 为1 ,tail 为5
/* 当我们get(2) 时,执行完毕之后,会将2节点移动到队伍尾部,变为1 <=> 3 <=>4 <=>5 <=>2 ,
/*
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 , afterNodeInsertion 中因为调用了removeEldestEntry , 所以当我们决定启用LRU之后,需要重写removeEldestEntry 方法,来执行淘汰策略,否则原方法默认 返回false 。
//evict put 调用时候默认传入true ,removeEldestEntry 这个方法默认返回false.
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
//这里的remov 方法仍然为HashMap 中方法。执行remove 方法,删除双链表头节点元素。
removeNode(hash(key), key, null, false, true);
}
}
//重写淘汰策略
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > INITIAL_CAPACITY;
}
3.2 get 方法分析
get 方法依然是HashMap 方法,由于我们重写了afterNodeAccess ,当我们查询过之后,会调用afterNodeAccess 将查询过的元素移动到队列尾部。
public V get(Object key) {
Node<K,V> e;
//这里的getNode 是继承自hashmap ,不做多描述
if ((e = getNode(hash(key), key)) == null)
return null;
//关键在这里,是否实现了LRU ,我们设置为TRUE
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
通过以上一遍流程,明白了LRU 算法的工作逻辑,接下来就来自己动手实践.
4. 动手实现LRU 算法。
public class LRUCache<K,V> {
static class Node<K,V>{
K key ;
V value ;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
Node<K,V> before ;
Node<K,V> after ;
}
private int capacity = 2<<4 ;
public int count = 0 ;
private HashMap<K , Node<K,V>> map ;
private Node<K,V> head ;
private Node<K,V> tail;
public LRUCache(int capacity) {
this() ;
this.capacity = capacity;
}
public LRUCache() {
map = new HashMap<>() ;
}
//get 获取
public V get(K key){
Node<K,V> node = map.get(key) ;
if(node == null)
return null ;
moveNodeToLast(node);
return node.value ;
}
public void put(K key ,V value){
Node<K ,V> node = map.get(key) ;
if(node == null){
//新建node,查看长度是否超过capacity
node = new Node<K, V>(key ,value) ;
//超过了,remove head
if(map.size() >= capacity){
removeHeadNode() ;
count -- ;
}
//新建之后将节点移动到尾部
setTailNode(node);
count ++ ;
}else {
node.value = value ;
map.put(key ,node) ;
//访问之后,移动到尾节点
moveNodeToLast(node);
}
map.put(key ,node) ;
}
//删除头节点
private void removeHeadNode() {
Node<K,V> e = map.get(head.key) ;
map.remove(head.key) ;
//保存p节点
Node<K,V> p = e ,a = p.after ,b = p.before ;
//断开前后链接
p.before = p.after = null ;
if(b == null )
head = a ;
else
b.after = a ;
if(a == null )
tail = b ;
else
a.before = b ;
}
//设置末尾指针
public void setTailNode(Node<K,V> node){
Node<K,V> last = tail ;
tail = node ;
//tai 为空的时候,初始化
if(last == null ){
head = node ;
}
else{
last.after = tail;
tail.before = last ;
}
}
//put 调用时候移动到tail
public void moveNodeToLast(Node<K,V> node){
Node<K,V> e = node ,a = node.after ,b = node.before ;
Node<K,V> last = tail ;
//当它不为末尾节点时候
if(last != node ){
if(b == null ){
head = a ;
}else{
b.after = a ;
}
if(a != null ){
a.before = b ;
}else{
last = b ;
}
//链接尾节点和新节点
if(last == null ){
head = e ;
}else{
last.after = e ;
e.before = last ;
}
tail = e ;
}
}
}
5.测试代码
不涉及线程安全的情况下,已经符合LRU 算法的基本要求。打完收工!