使用LinkedHashMap实现固定大小的LRU缓存
1. 什么是LRU?
LRU是”Least Recently Used”的缩写,意为”最近最少使用”。LRU缓存是一种常用的缓存淘汰算法,它的核心思想是:当缓存满时,优先淘汰最近最少使用的项目。
LRU缓存的工作原理:
-
新数据插入到缓存头部 -
每当缓存命中(即缓存数据被访问),则将数据移到缓存头部 -
当缓存满时,将链表尾部的数据丢弃
LRU算法的理论基础:
LRU算法基于”时间局部性原理”(Principle of Temporal Locality),该原理指出,如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。这一原理在计算机科学中广泛应用,例如在操作系统的页面置换算法中。
LRU的应用场景:
-
数据库缓存:减少对数据库的直接访问,提高查询速度 -
Web应用:缓存经常访问的页面或数据 -
硬件设计:CPU缓存的替换策略 -
操作系统:页面置换算法
2. LinkedHashMap与LRU缓存
LinkedHashMap的特性:
LinkedHashMap是Java集合框架中的一个类,它继承自HashMap,但在内部维护了一个双向链表,用于保持插入顺序或访问顺序。
关键特性:
-
可选的排序模式:插入顺序(默认)或访问顺序 -
预测遍历顺序:可以按照特定顺序遍历元素 -
性能:大部分操作的时间复杂度为O(1)
LinkedHashMap如何支持LRU:
LinkedHashMap通过以下机制支持LRU缓存的实现:
-
访问顺序:通过构造函数的accessOrder参数设置为true,启用访问顺序模式 -
自动重排序:每次访问元素时,该元素会被移到链表末尾(最近使用) -
removeEldestEntry方法:允许在插入新元素时,决定是否删除最老的元素
继承LinkedHashMap并重写removeEldestEntry方法:
要实现LRU缓存,我们需要:
-
创建一个新类,继承LinkedHashMap -
在构造函数中,设置LinkedHashMap的访问顺序为true -
重写removeEldestEntry方法,当map中的元素个数超过指定容量时返回true
3. 代码实现与深入分析
代码实现:
以下是一个简洁的LRU缓存实现,包含了基本功能和性能监控:
LRUCache.java
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @desc: 使用LinkedHashMap自定义LRU缓存实现
* @author: shy
* @date: 2024/08/26 10:03
*/
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
// 命中数(性能监控)
private int hits = 0;
// 未命中数(性能监控)
private int misses = 0;
public LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
@Override
public V get(Object key) {
V value = super.get(key);
if (value != null) {
hits++;
} else {
misses++;
}
return value;
}
public double getHitRate() {
int total = hits + misses;
return total == 0 ? 0 : (double) hits / total;
}
}
MapTest.java
public class MapTest {
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
System.out.println(cache); // 输出: {1=one, 2=two, 3=three}
cache.get(1);
System.out.println(cache); // 输出: {2=two, 3=three, 1=one}
cache.put(4, "four");
System.out.println(cache); // 输出: {3=three, 1=one, 4=four}
// 输出缓存命中率
System.out.println("Hit rate: " + cache.getHitRate());
}
}
执行结果

代码分析:
-
简洁实现:通过继承LinkedHashMap,我们只需要很少的代码就能实现LRU缓存的核心功能。 -
容量控制:重写removeEldestEntry方法,确保缓存大小不超过指定容量。 -
访问顺序:在构造函数中设置accessOrder为true,确保元素按访问顺序排列。 -
性能监控:添加了简单的命中率计算功能,有助于评估缓存效果。 -
泛型支持:使用泛型实现,增加了代码的灵活性和复用性。
4. LinkedHashMap实现LRU的优势与劣势
优势:
-
实现简单:
-
利用Java标准库,无需额外依赖 -
代码量少,易于理解和维护 -
性能较好:
-
大多数操作时间复杂度为O(1) -
内部使用哈希表,提供快速的查找性能 -
功能完整:
-
自动维护访问顺序 -
支持快速的插入和删除操作 -
灵活性:
-
可以轻松扩展,添加自定义功能(如上面的命中率计算) -
支持泛型,可用于各种数据类型
劣势:
-
内存占用:
-
比普通HashMap占用更多内存,因为需要维护双向链表 -
对于大容量缓存,可能会成为性能瓶颈 -
并发性能:
-
默认非线程安全,在多线程环境下需要额外的同步机制 -
全局同步可能导致高并发场景下的性能问题 -
功能局限:
-
不支持过期时间等高级特性 -
缺乏分布式缓存支持 -
扩展性限制:
-
继承自LinkedHashMap,可能限制了与其他类的集成 -
在复杂系统中,可能需要更灵活的接口设计
5. 实际应用中的注意事项
-
缓存大小选择:
-
需要根据实际应用场景和可用内存来确定 -
考虑缓存命中率和系统性能的平衡 -
并发处理:
-
在多线程环境中,需要注意同步问题 -
考虑使用 Collections.synchronizedMap() 包装 LRUCache,或使用 ConcurrentHashMap 的变体 -
缓存预热:
-
在系统启动时,可以预先加载常用数据到缓存中 -
有助于提高系统初期的响应速度 -
缓存一致性:
-
当底层数据发生变化时,需要及时更新或失效缓存 -
考虑实现缓存更新策略(如写透、延迟写入等) -
监控和调优:
-
实现缓存命中率、占用空间等指标的监控 -
根据监控数据定期调整缓存策略
6. 替代方案和进阶技巧
-
Guava Cache:
-
Google的Guava库提供了更强大的缓存实现 -
支持过期时间、自动加载、最大大小限制等特性 -
Caffeine:
-
高性能的Java缓存库,在许多方面超越了Guava Cache -
提供了更灵活的配置选项和更好的并发性能 -
多级缓存:
-
结合内存缓存和分布式缓存(如Redis) -
可以平衡访问速度和数据容量 -
自定义驱逐策略:
-
除LRU外,还可以实现LFU(最不经常使用)、FIFO等策略 -
根据实际应用需求选择或组合不同的策略 -
hutool-cache:
-
功能丰富的缓存工具类
-
支持设置缓存的过期时间和最大容量 -
支持灵活地控制缓存的生命周期和大小
通过使用LinkedHashMap实现固定大小的LRU缓存的实现,展示了如何使用LinkedHashMap创建一个简单而有效的LRU缓存。这个实现保持了代码的简洁性,同时仍然提供了基本的性能监控功能。在实际应用中,可以根据具体需求进行进一步的扩展和优化。
原文始发于微信公众号(shy好好学习):使用LinkedHashMap实现固定大小的LRU缓存
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/300662.html