LRU介绍
LRU(Least Recently Used,最近最少使用)是一种经典的缓存淘汰策略。它的核心思想非常符合直觉:如果一个数据在最近一段时间没有被访问过,那么它在将来被访问的可能性也很小 。因此,当缓存容量达到上限时,系统会优先淘汰那个"最长时间未被使用"的数据。

为了让你更直观地理解,我们可以把 LRU 缓存想象成一个容量固定的书架:
- 规则:书架只能放 N 本书。
- 取书/放书 :每当你阅读或放入一本书,就把这本书放到书架的最右侧(代表"最近刚用过")。
- 淘汰机制 :当书架满了,而你需要放入一本新书时,必须把书架最左侧那本"最久没碰过"的书扔掉,腾出空间给新书。
相关代码
数据结构LinkedHashMap实现
java
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache {
// 缓存的最大容量
private final int capacity;
// 核心容器:使用 LinkedHashMap 存储键值对
// LinkedHashMap 底层是"哈希表 + 双向链表",默认按照"插入顺序"维护元素
// 这里的技巧是:通过"先删后插"的操作,手动把被访问的元素挪到链表末尾
private final Map<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
}
public int get(int key) {
// 核心动作:尝试从 map 中移除 key
// remove(key) 会返回被删除的 value;如果 key 不存在,返回 null
// 这一步不仅获取了值,还顺带把该节点从双向链表中"摘除"了
Integer value = cache.remove(key);
if (value != null) {
// 如果 value 不为 null,说明 key 存在(缓存命中)
// 重新执行 put 操作。因为是新插入,LinkedHashMap 会把这个元素放到链表的【最末尾】
// 这就实现了"刷新热度",将其标记为"最近刚用过"
cache.put(key, value);
return value;
}
// key 不在 cache 中,直接返回 -1
return -1;
}
public void put(int key, int value) {
// 同样先尝试移除旧节点。如果返回值不为 null,说明是"更新操作"
if (cache.remove(key) != null) {
// 更新值并重新插入到链表末尾(刷新热度)
// 因为只是更新,缓存大小没变,不需要判断容量,直接返回即可
cache.put(key, value);
return;
}
// 执行到这里,说明 key 之前不存在,是一个"全新插入"的操作
// 插入前必须先判断缓存是否已满
if (cache.size() == capacity) {
// cache 满了,需要淘汰"最久未使用"的元素
// 在 LinkedHashMap 的插入顺序模式下,迭代器的【第一个元素】就是最早插入/最久未更新的
// 也就是链表【头部】的节点,即我们要淘汰的 LRU 节点
Integer eldestKey = cache.keySet().iterator().next();
// 删除链表头部的这个最旧节点,腾出空间
cache.remove(eldestKey);
}
// 将新节点插入到链表【末尾】,标记为最新使用
cache.put(key, value);
}
}
手撕双向链表实现
java
class LRUCache {
private static class Node {
int key, value;
Node prev, next;
Node(int k, int v) {
key = k;
value = v;
}
}
private final int capacity;
private final Node dummy = new Node(0, 0); // 哨兵节点
private final Map<Integer, Node> keyToNode = new HashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
dummy.prev = dummy;
dummy.next = dummy;
}
public int get(int key) {
Node node = getNode(key); // getNode 会把对应节点移到链表头部
return node != null ? node.value : -1;
}
public void put(int key, int value) {
Node node = getNode(key); // getNode 会把对应节点移到链表头部
if (node != null) { // 有这本书
node.value = value; // 更新 value
return;
}
node = new Node(key, value); // 新书
keyToNode.put(key, node);
pushFront(node); // 放到最上面
if (keyToNode.size() > capacity) { // 书太多了
Node backNode = dummy.prev;
keyToNode.remove(backNode.key);
remove(backNode); // 去掉最后一本书
}
}
// 获取 key 对应的节点,同时把该节点移到链表头部
private Node getNode(int key) {
if (!keyToNode.containsKey(key)) { // 没有这本书
return null;
}
Node node = keyToNode.get(key); // 有这本书
remove(node); // 把这本书抽出来
pushFront(node); // 放到最上面
return node;
}
// 删除一个节点(抽出一本书)
private void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
}
// 在链表头添加一个节点(把一本书放到最上面)
private void pushFront(Node x) {
x.prev = dummy;
x.next = dummy.next;
x.prev.next = x;
x.next.prev = x;
}
}
常用函数
| 常用函数 | 基础功能 | 在 LRU 中的特殊作用 |
|---|---|---|
remove(Object key) |
删除指定 key 的映射,返回对应的 value;若 key 不存在则返回 null。 |
实现"移动节点"的核心。通过先 remove 再 put,利用 LinkedHashMap 的插入顺序特性,手动把被访问的元素从链表中间"挪"到了链表末尾,从而刷新其热度。 |
put(K key, V value) |
插入或更新键值对。如果 key 已存在则覆盖 value;如果 key 不存在则插入到链表末尾。 | 标记"最近使用"。无论是 get 命中后的重新插入,还是全新的 put 操作,都会把元素放到链表的最尾部,代表它是当前最新的数据。 |
keySet().iterator().next() |
获取 Map 中所有 key 的集合,并拿到迭代器的第一个元素。 | 精准定位"淘汰目标"。在默认的插入顺序模式下,迭代器的第一个元素永远是最早被插入且后续未被更新过的节点,也就是链表头部的"最久未使用"元素。 |
size() |
返回 Map 中当前存储的键值对数量。 | 容量守门员。在插入新元素前判断 size() == capacity,确保缓存不会超过预设上限,触发淘汰逻辑。 |