缓存淘汰之 LRU 算法的几种实现方案
LRU,全称 Least Recently Used,即最近最少使用。它是一种常用的缓存淘汰算法,也常用于虚拟内存管理。
核心思想:LRU 认为“最近被使用过的数据,在将来被再次访问的概率更高;而很久没被访问的数据,将来被访问的概率也更低”。因此,当缓存空间已满,需要淘汰旧数据来存入新数据时,它会优先淘汰那个“最近最少使用”的数据。
算法原理与数据结构
为了实现 LRU,我们需要高效地完成两件事:
-
快速查找:判断一个数据是否在缓存中。
-
快速排序:能清晰地记录数据的访问顺序(最近使用的和很久未使用的),并在需要时快速淘汰最久未使用的数据。
单纯使用数组或链表都无法同时满足这两点。因此,LRU 通常由 哈希表(Hash Table) 和 双向链表(Doubly Linked List) 两种数据结构结合实现。
双向链表(用于排序):
- 作用:模拟访问顺序。链表头部是 最近使用的(Most Recently Used, MRU),尾部是 最久未使用的(LRU)。
- 操作:
- 每当一个数据被访问(
get)或添加(put),它就被移动到链表头部。- 当需要淘汰数据时,直接删除链表尾部的节点。
- 为什么是双向链表? 因为删除一个节点(尤其是中间节点)需要知道其前驱节点,单链表无法在 O(1) 时间内完成。
哈希表(用于快速查找):
- 作用:提供 Key 到 链表节点 的快速映射。
- 操作:通过 Key 可以立刻找到对应的链表节点,从而在 O(1) 时间内获取值或定位节点以便将其移动到头部。
三种实现方案
可以选择链表或者是数组来构建
数组实现,构建数组,其中每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰(插入、删除,时间复杂度O(n))。
链表实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃(访问时间复杂度为O(n))。
链表+hash。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
1
2
3
4
5
6
7 // 通过HashMap中`key`存储Node的`key`,`value`存储Node来建立Map对Node的映射关系
class Node<K,V>{
private K key;
private V value;
private Node<K,V> prev;
private Node<K,V> next;
}
1、新数据插入到链表头部;
2、每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3、当链表满的时候,将链表尾部的数据丢弃。
基于数组实现
利用数组 + 时间戳 实现。避免了数组下标值的移动,但是需要遍历所有值,并比较时间戳大小。
1 | /** |
基于链表实现
利用链表快捷的删除和插入特性实现,不涉及数据的下标移动计算。
1 | /** |
基于链表+Hash实现
基于双链表和hash数组实现
其核心实现结合了哈希表(快速查找)和双向链表(维护访问顺序) 两种数据结构,使得所有操作都能在常数时间内完成。
HashMap:存储 key → Node,实现 O(1) 查找。
双向链表:维护访问顺序,头节点为最新访问,尾节点为最久未访问。
每次访问或插入,将节点移到头部;容量超限时,删除尾部节点。
1 | /** |
Java 中如何应用
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| LinkedHashMap | 简单、内置、代码少 | 非线程安全 | 简单单线程场景 |
| 手动双向链表 | 可控性强,适合面试 | 代码复杂,易出错 | 学习/面试/定制需求 |
| Guava Cache | 功能丰富,成熟稳定 | 性能略逊于 Caffeine | 一般项目 |
| Caffeine | 高性能,功能强,推荐生产使用 | 需要引入第三方依赖 | 生产环境高性能缓存 |
使用内置类:LinkedHashMap
最简单、最常用的方式。LinkedHashMap 内部维护了插入或访问顺序,通过重写 removeEldestEntry() 方法可以实现自动淘汰最久未使用的元素。
1 | import java.util.LinkedHashMap; |
使用第三方库
-
Guava
Guava 的
Cache支持多种淘汰策略(包括 LRU),还支持过期时间、加载函数等高级功能。1
2
3
4
5
6
7
8
9import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
Cache<Integer, String> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.build();
cache.put(1, "A");
String value = cache.getIfPresent(1); -
Caffeine
Caffeine 是目前 Java 生态中性能最好的本地缓存库,基于 Guava 改进,支持 Window TinyLFU 等更优算法。
1
2
3
4
5
6
7
8
9import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.build();
cache.put(1, "A");
String value = cache.getIfPresent(1);