方法一:基于LinkedHashMap实现LRU
使用 LinkedHashMap 实现 LRU(最近最少使用)缓存非常简单,因为 LinkedHashMap 本身提供了按访问顺序排序和移除最老条目的钩子方法。
实现原理
-
构造
LinkedHashMap时设置accessOrder=true这样每次调用
get或put时,被访问的条目会被移动到链表的末尾,链表头部就是最久未使用的条目。 -
重写
removeEldestEntry方法当
Map大小超过设定的最大容量时,返回true,LinkedHashMap就会自动移除头部的(最老的)条目。
代码示例
java
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity;
/**
* 构造 LRU 缓存
* @param maxCapacity 最大容量
*/
public LRUCache(int maxCapacity) {
// 初始容量:默认16,负载因子0.75,accessOrder=true 表示按访问顺序排序
super(16, 0.75f, true);
this.maxCapacity = maxCapacity;
}
/**
* 当 Map 大小超过最大容量时,移除最老的条目
* @param eldest 最老的条目
* @return 是否移除该条目
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxCapacity;
}
// 测试代码
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "A");
cache.put(2, "B");
cache.put(3, "C");
System.out.println(cache); // {1=A, 2=B, 3=C}
// 访问 1,它会被移动到末尾
cache.get(1);
System.out.println(cache); // {2=B, 3=C, 1=A}
// 插入新条目,此时容量超限,移除最老的 2
cache.put(4, "D");
System.out.println(cache); // {3=C, 1=A, 4=D}
}
}
关键点说明
-
super(16, 0.75f, true)第三个参数
true表示启用"访问顺序",而非默认的插入顺序。 -
removeEldestEntry每次
put之后会自动调用该方法,判断是否需要移除头部最老的条目。这里直接比较当前大小与最大容量。 -
线程不安全
上述实现不是线程安全的,如果需要在多线程环境下使用,可以包装为
Collections.synchronizedMap,或者使用ConcurrentLinkedHashMap(第三方库)。
扩展:泛型与线程安全版本
如果需要线程安全,可以这样包装:
java
LRUCache<Integer, String> cache = new LRUCache<>(3);
Map<Integer, String> synchronizedCache = Collections.synchronizedMap(cache);
但注意 removeEldestEntry 的调用会在同步块内部自动处理。
方法二:基于 Guava Cache 和 Caffeine
1. 添加依赖
Maven
xml
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.2.1-jre</version>
</dependency>
<!-- Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
Gradle
groovy
implementation 'com.google.guava:guava:33.2.1-jre'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
2. Guava Cache 示例(基于 maximumSize 的 LRU)
java
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;
public class GuavaLRUExample {
public static void main(String[] args) {
// 创建最大容量为 3 的缓存,基于访问顺序(LRU)
Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(3) // 最多 3 个条目
.expireAfterAccess(10, TimeUnit.MINUTES) // 可选:10分钟未访问则过期
.recordStats() // 可选:开启统计
.build();
// 写入数据
cache.put("key1", "A");
cache.put("key2", "B");
cache.put("key3", "C");
System.out.println(cache.asMap()); // {key1=A, key2=B, key3=C}
// 访问 key1(LRU 会将 key1 移到最近使用的位置)
cache.getIfPresent("key1");
// 插入新 key4,此时 size 将超过 3,最久未使用的 key2 会被淘汰
cache.put("key4", "D");
System.out.println(cache.asMap()); // {key3=C, key1=A, key4=D}
// 查看统计信息
System.out.println(cache.stats()); // 命中率等
}
}
关键点:
maximumSize(3)自动实现 LRU(基于访问顺序)。getIfPresent不会自动加载,若需要自动加载可以用LoadingCache并实现load方法。- 淘汰策略是异步执行的,最终一致。
3. Caffeine 示例(更现代,性能更好)
java
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
public class CaffeineLRUExample {
public static void main(String[] args) {
// 创建最大容量为 3 的缓存,基于访问顺序(LRU 是其 W-TinyLFU 的一部分)
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(3)
.expireAfterAccess(10, TimeUnit.MINUTES) // 可选
.recordStats()
.build();
cache.put("key1", "A");
cache.put("key2", "B");
cache.put("key3", "C");
System.out.println(cache.asMap()); // {key1=A, key2=B, key3=C}
// 访问 key1
cache.getIfPresent("key1");
// 插入 key4 -> 淘汰最久未使用的 key2
cache.put("key4", "D");
System.out.println(cache.asMap()); // {key3=C, key1=A, key4=D}
// 统计信息
System.out.println(cache.stats());
}
}
Caffeine 特有的高级用法(异步加载、手动加载等):
java
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
4. 如何验证 LRU 行为(访问顺序淘汰)
java
// 两个库行为一致
cache.put("a", "1");
cache.put("b", "2");
cache.put("c", "3");
cache.getIfPresent("a"); // 将 a 变成最近使用
cache.put("d", "4"); // 容量超限,淘汰最久未使用的 b
assert cache.asMap().containsKey("b") == false;
assert cache.asMap().containsKey("a") == true;
5. 线程安全说明
- Guava Cache 和 Caffeine 返回的
Cache实例都是线程安全的,内部使用了ConcurrentHashMap类似的并发控制,无需额外同步。 - 可以直接在多线程环境中共享使用。
6. 何时用 Guava,何时用 Caffeine?
- Guava:成熟稳定,依赖较广,适合老项目或团队已经使用 Guava。
- Caffeine :性能更高(尤其是高并发读多写少场景),命中率更高(W-TinyLFU 算法),内存占用更低。推荐新项目使用 Caffeine。
Spring 5+ 和 Spring Boot 2.x 已将 Caffeine 作为默认的 CacheManager 实现(替代 Guava)。