一、算法实现
import java.util.HashMap;
/**
* LRU(Least Recently Used):最近最少使用
* 核心思想:最近被访问的数据将来被访问的概率很大——FIFO思想的运用
* 算法描述:(队列越靠近顶部数据越陈旧)
* 1、缓存有余时:将数据直接放到队列底部
* 2、缓存用完时:
* 1)当缓存中存在待访问的数据时,将该数据移到缓存底部
* 2)当缓存中不存在待访问的数据时,将队列顶部的数据移出,并将新数据插入到队列底部
* <p>
* 使用HashMap + 双向链队列实现LRU算法
* 1、HashMap用来存储具体的缓存数据
* 2、双向链表将HashMap中的数据以访问的时间顺序链接起来
*
* @author happy
* @date 2019/11/21
*/
public class LruCache {
/**
* 缓存容量
*/
private int capacity;
/**
* 以使用的缓存指针
*/
private int used = 0;
private HashMap<String, DLinkedNode> cache;
/**
* 双向队列的头尾指针
*/
private DLinkedNode head, tail;
public LruCache(int capacity) {
this.capacity = capacity;
cache = new HashMap<>(capacity);
head = new DLinkedNode();
tail = new DLinkedNode();
// 初始化双向队列为空
head.pre = null;
head.next = tail;
tail.pre = head;
tail.next = null;
}
/**
* 将数据添加到缓存中
*
* @param key 待缓存数据的键
* @param value 待缓存数据的值
*/
public void set(String key, String value) {
if (key == null) {
return;
}
DLinkedNode cachedNode = cache.get(key);
if (cachedNode == null) {
// 将数据封装后放入到缓存中
DLinkedNode newNode = new DLinkedNode();
newNode.value = value;
cache.put(key, newNode);
used++;
if (used > capacity) {
// 移除队首元素
DLinkedNode removedNode = pop();
remove(removedNode);
// 从缓存中移除该数据
cache.remove(key);
used--;
}
// 将新结点添加到队尾
append(newNode);
} else {
// 更新缓存值
cachedNode.value = value;
moveToHead(cachedNode);
}
}
/**
* 从缓存中取出数据
*
* @param key 缓存对应的键
* @return 从缓存中取出的值,如果key为null或缓存中不存在该key对应的值则返回null
*/
public String get(String key) {
if (key == null) {
return null;
}
DLinkedNode cacheNode = cache.get(key);
if (cacheNode == null) {
return null;
}
// 将结点移动到队尾
moveToHead(cacheNode);
return cacheNode.value;
}
/**
* 将指定结点移到队列尾部
*
* @param cachedNode 待移动到队列尾部的结点
*/
private void moveToHead(DLinkedNode cachedNode) {
// 将该结点从队列中删除
remove(cachedNode);
append(cachedNode);
}
/**
* 将指定结点从双向队列中移除
*
* @param cachedNode 待从队列中移除的结点
*/
private void remove(DLinkedNode cachedNode) {
if (cachedNode == null) {
return;
}
DLinkedNode preNode = cachedNode.pre;
DLinkedNode nextNode = cachedNode.next;
cachedNode.pre = null;
cachedNode.next = null;
preNode.next = nextNode;
nextNode.pre = preNode;
}
/**
* 将指定结点追加到队列尾部
*
* @param newNode 待追加的结点
*/
private void append(DLinkedNode newNode) {
if (newNode == null) {
return;
}
head.next.pre = newNode;
newNode.next = head.next;
newNode.pre = head;
head.next = newNode;
}
/**
* @return 队首元素
*/
private DLinkedNode pop() {
return tail.pre;
}
/**
* 链表结点
*/
private static class DLinkedNode {
String value;
DLinkedNode pre;
DLinkedNode next;
}
}
二、算法图解
Java 的 LinkedHashMap 为什么要将最近访问的 entry 放到链表的末尾?
LRUCache entry reordering when using get
LinkedHashMap shows how to implement it without adding separate backward-moving iterators: by putting the recent elements at the end
在调用 put 插入新 key-value 时,使用尾插法将节点插入到双向链表的尾部
/*
代码来自: java.util.LinkedHashMap#newNode
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<>(hash, key, value, e);
linkNodeLast(p);
return p;
}
// 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;
}
}
所以访问过的节点「相当于」是新节点,与插入时的新节点逻辑保持一致,放入到队尾。