当前位置: 首页 > news >正文

使用LinkedHashMap实现固定大小的LRU缓存

使用LinkedHashMap实现固定大小的LRU缓存

1. 什么是LRU?

LRU是"Least Recently Used"的缩写,意为"最近最少使用"。LRU缓存是一种常用的缓存淘汰算法,它的核心思想是:当缓存满时,优先淘汰最近最少使用的项目。

LRU缓存的工作原理:

  1. 新数据插入到缓存头部
  2. 每当缓存命中(即缓存数据被访问),则将数据移到缓存头部
  3. 当缓存满时,将链表尾部的数据丢弃

LRU算法的理论基础:

LRU算法基于"时间局部性原理"(Principle of Temporal Locality),该原理指出,如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。这一原理在计算机科学中广泛应用,例如在操作系统的页面置换算法中。

LRU的应用场景:

  1. 数据库缓存:减少对数据库的直接访问,提高查询速度
  2. Web应用:缓存经常访问的页面或数据
  3. 硬件设计:CPU缓存的替换策略
  4. 操作系统:页面置换算法

2. LinkedHashMap与LRU缓存

LinkedHashMap的特性:

LinkedHashMap是Java集合框架中的一个类,它继承自HashMap,但在内部维护了一个双向链表,用于保持插入顺序或访问顺序。

关键特性:

  1. 可选的排序模式:插入顺序(默认)或访问顺序
  2. 预测遍历顺序:可以按照特定顺序遍历元素
  3. 性能:大部分操作的时间复杂度为O(1)

LinkedHashMap如何支持LRU:

LinkedHashMap通过以下机制支持LRU缓存的实现:

  1. 访问顺序:通过构造函数的accessOrder参数设置为true,启用访问顺序模式
  2. 自动重排序:每次访问元素时,该元素会被移到链表末尾(最近使用)
  3. removeEldestEntry方法:允许在插入新元素时,决定是否删除最老的元素

继承LinkedHashMap并重写removeEldestEntry方法:

要实现LRU缓存,我们需要:

  1. 创建一个新类,继承LinkedHashMap
  2. 在构造函数中,设置LinkedHashMap的访问顺序为true
  3. 重写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;}@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {return size() > capacity;}@Overridepublic 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());}
}
执行结果

在这里插入图片描述

代码分析:

  1. 简洁实现:通过继承LinkedHashMap,我们只需要很少的代码就能实现LRU缓存的核心功能。
  2. 容量控制:重写removeEldestEntry方法,确保缓存大小不超过指定容量。
  3. 访问顺序:在构造函数中设置accessOrder为true,确保元素按访问顺序排列。
  4. 性能监控:添加了简单的命中率计算功能,有助于评估缓存效果。
  5. 泛型支持:使用泛型实现,增加了代码的灵活性和复用性。

4. LinkedHashMap实现LRU的优势与劣势

优势:

  1. 实现简单:

    • 利用Java标准库,无需额外依赖
    • 代码量少,易于理解和维护
  2. 性能较好:

    • 大多数操作时间复杂度为O(1)
    • 内部使用哈希表,提供快速的查找性能
  3. 功能完整:

    • 自动维护访问顺序
    • 支持快速的插入和删除操作
  4. 灵活性:

    • 可以轻松扩展,添加自定义功能(如上面的命中率计算)
    • 支持泛型,可用于各种数据类型

劣势:

  1. 内存占用:

    • 比普通HashMap占用更多内存,因为需要维护双向链表
    • 对于大容量缓存,可能会成为性能瓶颈
  2. 并发性能:

    • 默认非线程安全,在多线程环境下需要额外的同步机制
    • 全局同步可能导致高并发场景下的性能问题
  3. 功能局限:

    • 不支持过期时间等高级特性
    • 缺乏分布式缓存支持
  4. 扩展性限制:

    • 继承自LinkedHashMap,可能限制了与其他类的集成
    • 在复杂系统中,可能需要更灵活的接口设计

5. 实际应用中的注意事项

  1. 缓存大小选择:

    • 需要根据实际应用场景和可用内存来确定
    • 考虑缓存命中率和系统性能的平衡
  2. 并发处理:

    • 在多线程环境中,需要注意同步问题
    • 考虑使用 Collections.synchronizedMap() 包装 LRUCache,或使用 ConcurrentHashMap 的变体
  3. 缓存预热:

    • 在系统启动时,可以预先加载常用数据到缓存中
    • 有助于提高系统初期的响应速度
  4. 缓存一致性:

    • 当底层数据发生变化时,需要及时更新或失效缓存
    • 考虑实现缓存更新策略(如写透、延迟写入等)
  5. 监控和调优:

    • 实现缓存命中率、占用空间等指标的监控
    • 根据监控数据定期调整缓存策略

6. 替代方案和进阶技巧

  1. Guava Cache:

    • Google的Guava库提供了更强大的缓存实现
    • 支持过期时间、自动加载、最大大小限制等特性
  2. Caffeine:

    • 高性能的Java缓存库,在许多方面超越了Guava Cache
    • 提供了更灵活的配置选项和更好的并发性能
  3. 多级缓存:

    • 结合内存缓存和分布式缓存(如Redis)
    • 可以平衡访问速度和数据容量
  4. 自定义驱逐策略:

    • 除LRU外,还可以实现LFU(最不经常使用)、FIFO等策略
    • 根据实际应用需求选择或组合不同的策略
  5. hutool-cache:

    • 功能丰富的缓存工具类
    • 支持设置缓存的过期时间和最大容量
    • 支持灵活地控制缓存的生命周期和大小

通过使用LinkedHashMap实现固定大小的LRU缓存的实现,展示了如何使用LinkedHashMap创建一个简单而有效的LRU缓存。这个实现保持了代码的简洁性,同时仍然提供了基本的性能监控功能。在实际应用中,可以根据具体需求进行进一步的扩展和优化。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 三维重建学习
  • 计算机基础知识复习8.30
  • 若依脚手架 创建一个系统 his医院信息管理系统
  • 数据库表的分类
  • 数字芯片设计验证经验分享系列文章(第四部分):将ASIC IP核移植到FPGA上——如何测试IP核的功能和考虑纯电路以外的其他因素
  • 物品租赁​​​​​​​|基于SprinBoot+vue的物品租赁​​​​​​​系统(源码+数据库+文档)
  • Hive SQL
  • 只有十几岁,能不能学黑客技术或网络安全?
  • UE5 摄像机图像采集到材质 映射到 UI 和 物体表面
  • 【C++】RAII思想与智能指针原理——有效避免内存泄露问题
  • 数字时代的内容安全治理:审核与管理的艺术
  • 机器学习 第6章 支持向量机
  • 【QT | 开发环境搭建】Linux系统(Ubuntu 18.04) 安装 QT 5.12.12 开发环境
  • AiThinkerIDE_V1.5.2+AiThinkerProjectForESP开发环境安装
  • 【flask框架搭建服务器demo】Python 使用轻量级 Flask 框架搭建 Web 服务器可视化数据库数据demo
  • 【node学习】协程
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • conda常用的命令
  • Javascript基础之Array数组API
  • Java比较器对数组,集合排序
  • LeetCode算法系列_0891_子序列宽度之和
  • Mysql5.6主从复制
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • Python实现BT种子转化为磁力链接【实战】
  • Python学习笔记 字符串拼接
  • Travix是如何部署应用程序到Kubernetes上的
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • Vue.js-Day01
  • 彻底搞懂浏览器Event-loop
  • 第三十一到第三十三天:我是精明的小卖家(一)
  • 浮现式设计
  • 记录一下第一次使用npm
  • 精彩代码 vue.js
  • 聊一聊前端的监控
  • 入门级的git使用指北
  • 双管齐下,VMware的容器新战略
  • 智能情侣枕Pillow Talk,倾听彼此的心跳
  • #WEB前端(HTML属性)
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • %3cli%3e连接html页面,html+canvas实现屏幕截取
  • (2)(2.10) LTM telemetry
  • (2)STL算法之元素计数
  • (20)目标检测算法之YOLOv5计算预选框、详解anchor计算
  • (39)STM32——FLASH闪存
  • (echarts)echarts使用时重新加载数据之前的数据存留在图上的问题
  • (vue)页面文件上传获取:action地址
  • (Windows环境)FFMPEG编译,包含编译x264以及x265
  • (编程语言界的丐帮 C#).NET MD5 HASH 哈希 加密 与JAVA 互通
  • (论文阅读23/100)Hierarchical Convolutional Features for Visual Tracking
  • (亲测)设​置​m​y​e​c​l​i​p​s​e​打​开​默​认​工​作​空​间...
  • (数据大屏)(Hadoop)基于SSM框架的学院校友管理系统的设计与实现+文档
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • (轉貼)《OOD启思录》:61条面向对象设计的经验原则 (OO)
  • ***原理与防范
  • .NET Framework .NET Core与 .NET 的区别