Java中自定义LRU缓存详解
作者:晓之木初
这篇文章主要介绍了Java中自定义LRU缓存详解,基于LRU算法的缓存系统,可以在达到缓存容量上限时,清理最近最少使用的数据,为新的数据的插入腾出空间,需要的朋友可以参考下
1. LRU算法
- 在计算机领域,LRU算法的应用非常多,最常见的就是LRU缓存
- LRU:Least Recently USed,最近最少使用
- 英文和中文存在差异,如果只看中文,貌似RLU更合适
- 基于LRU算法的缓存系统,可以在达到缓存容量上限时,清理最近最少使用的数据,为新的数据的插入腾出空间
- leetcode上,也有对应的LRU缓存算法题:146. LRU 缓存机制
- 牛客上,蚂蚁金服的面试题库,LRU缓存也赫然在列
- 题目要求大概如下:
- 设计和实现一个LRU算法的缓存数据结构。需要实现两个操作:get和set
- 获取数据 get(key) :如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
- 写入数据 put(key, value) :
- 如果关键字已经存在,则变更其数据值;
- 如果关键字不存在,则插入该组<key, value>。
- 当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
2. 继承LinkedHashMap实现LRU缓存
通过对LinkedHashMap的学习,我们了解到:
与HashMap不同,LinkedHashMap作为链表形式的哈希表,支持元素的插入顺序或访问顺序
使用访问顺序时,通过重写removeEldestEntry()方法,可以删除最近最少使用的键值对
因此,可以通过继承LinkedHashMap、重写removeEldestEntry() 方法,实现LRU缓存
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache extends LinkedHashMap<Integer, Integer> {
private int capacity;
public LRUCache(int capacity) {
// true表示按访顺序存储键值对,最近访问的在尾部,最近最少访问在头部
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
public int get(int key) {
// 根据题目要求,不存在key时,不能直接返回null值,而是需要返回默认值-1
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
}3. 自定义LRU缓存
- 如果在面试时,碰到该题,应该先和面试官确认,是否能使用现成的数据结构去实现
- 如果面试官明确要求说,需要自己去实现LRU缓存,不能使用现成的数据结构,那么时候展现你的实力了
最原始的想法:
- 既然LinkedHashMap都是双向链表 + HashMap实现的,那我自己定义一个双向链表实现类似的功能
class DLinkedNode{
private int key;
private int val;
private DLinkNode prev;
private DLinkNode next;
}- 基于双向链表,可以在 O ( 1 ) O(1) O(1)的时间内快速增加、删除节点
- 但在确定节点位置时,需要从头到尾遍历链表
- 就算使用双指针左右开弓,在定位节点时,依然存在很大的开销
思路进阶
- 借助HashMap,以O ( 1 ) O(1)O(1)的时间复杂度快速get或put数据
- 同时,规定:最近访问的节点放在链表的头部,最近最少访问的节点放在链表的尾部
- 如果头部或尾部直接存储数据,则在实现时需要考虑节点是否为头节点或尾结点的情况
- 因此,直接创建dummy的head和tail节点,以减少编写代码的工作量

最终的代码实现
通过上述分析,代码如下
import java.util.HashMap;
public class LRUCache{
private HashMap<Integer, DLinkedNode> map;
private DLinkedNode head;
private DLinkedNode tail;
private int capacity;
private int size;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
// 初始化带dummy节点的双向链表
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = map.get(key);
if (node == null) {
return -1;
}
// 被访问,需要从当前位置移动到头部
if (head.next != node){
removeNode(node);
insertToHead(node);
}
return node.val;
}
public void put(int key, int value) {
DLinkedNode node = map.get(key);
// 如果存在,直接更新值
if (node != null) {
node.val = value;
// 被访问,需要从当前位置移动到头部
if (head.next != node) {
removeNode(node);
insertToHead(node);
}
} else {
// 插入前,先判断是否需要腾出空间
if (size == capacity) {
// 从链表中删除尾结点
DLinkedNode last = tail.prev;
removeNode(last);
// 从map中移除记录
map.remove(last.key);
size--;
}
// 新建节点并插入
DLinkedNode newNode = new DLinkedNode(key, value);
insertToHead(newNode);
map.put(key, newNode);
size++;
}
}
// 插入节点一定是在头部
public void insertToHead(DLinkedNode node) {
// 分别建立与head.next和head的关联
DLinkedNode next = head.next;
node.next = next;
next.prev = node;
head.next = node;
node.prev = head;
}
// 删除指定节点
public void removeNode(DLinkedNode node) {
DLinkedNode prev = node.prev;
DLinkedNode next = node.next;
prev.next = next;
next.prev = prev;
// 断开引用,帮助GC
node.prev = null;
node.next = null;
}
}
class DLinkedNode{
int key;
int val;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {
}
public DLinkedNode(int key, int val) {
this.key = key;
this.val = val;
}
}几点注意事项:
- 节点移动到头部情况:get时,节点被访问;put时,节点的值被更新。put时的情况,容易被忽略
- 新增节点时,按照题目要求是先删除最久未使用的节点,并非先插入再删除
- 注意双向链表和HashMap的联动,删除或新增节点,HashMap中也要删除或新增记录
到此这篇关于Java中自定义LRU缓存详解的文章就介绍到这了,更多相关Java的LRU缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
