剖析LRU算法及LinkedHashMap源码实现机制

2024-09-06 10:08

本文主要是介绍剖析LRU算法及LinkedHashMap源码实现机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、简述

LRU(Least Recently Used),注意L代表的不是Latest,翻译成中文通常叫:近期最少使用算法、最近最少使用算法。LRU与LFU(Least Frequently Used)、FIFO(First In First Out)是3种常用的缓存算法(页面置换算法)。缓存算法的应用场景有很多,例如操作系统在物理内存不足时触发的磁盘交换、CPU中L1、L2、L3缓存淘汰替换、超市中畅销货品在货架的摆放位置优化等。缓存算法的实现和变种也有很多,这里只针对LRU算法抛砖引玉。

二、算法原理

缓存算法的根本目的,是淘汰掉“最不常用”的元素。LRU算法淘汰的是截止当前缓存区中最久没有被访问过的元素,这意味着“最后一次访问时间”是LRU算法决定是否淘汰元素的唯一标准。

因此,假设我们记录下每个元素最后一次被访问的时间戳,并按照此时间戳早晚进行排序,那么,时间最早的元素应该被淘汰掉(如图1)。所以,将LRU准确地翻译成中文,应该是:距今最久未用淘汰算法。

图1:最后一次访问时间最早的元素首先被淘汰

这里补充一点,LRU算法并不是绝对公平的。举个例子,假如某个元素仅仅在淘汰执行前被访问了1次,在之前的数百万次的请求中从未被访问,按照LRU的算法逻辑,它仍然会留存下来,这其实无法准确描述“是否常用”这一特性。“是否常用”其实在不同的业务场景有不同的定义,大家可以细细体会。

三、LRU的实现机制

依照LRU算法原理,其实现机制并不复杂,仅仅需要根据元素的最后一次访问时间排序即可。但当运用在1个生产环境的缓存系统中时,会面临以下几个工程问题:

  1. 在缓存访问频次极高的情况下,时间戳即使精确到纳秒,依然存在大量的相同时间戳,排序无效。若采用递增ID替代时间戳,则存在溢出的风险。
  2. 每次进行缓存淘汰时,都需要将缓存区所有元素进行排序。当元素数极多时,缓存系统的性能将急剧下降,CPU耗费极高。
  3. 在元素本身占用内存不大的情况下,附加的“时间戳/自增ID”甚至在内存占用上喧宾夺主。

最佳的实现思路是利用链表的有序特性,将缓存元素的“按最后一次访问时间戳排序”巧妙地转换为其在链表中的相对顺序。因为LRU算法关心的并不是元素的绝对访问时间,而是元素被访问的先后顺序。即:元素被命中时,移到链表头。执行淘汰时,从链表尾开始淘汰。因此,LRU算法实现的核心数据结构是:循环链表。

但对于一个<K,V>缓存系统而言,仅有链表结构是不够的。链表虽然解决了缓存淘汰问题,但缓存访问却需要每次都从链表头开始遍历。JDK中的java.util.LinkedHashMap给出了最佳的实现(链表也是Linked这一前缀的由来),同时解决了元素的快速访问与缓存淘汰问题。

四、JDK中LinkedHashMap对LRU的实现源码分析

1、概述

从Oracle的JDK源码的注释中可以看到,从JDK1.4开始,Josh Bloch就提供了LinkedHashMap供开发者使用。LinkedHashMap继承自HashMap,拥有<K,V>结构的同时还提供了缓存淘汰的扩展。

 

2、LinkedHashMap的使用示例(LRU效果)

 

上面的代码打印结果为:abce,很正常,按照加入的顺序打印

打印结果为:ceab ,可以看出LinkedHashMap构造参数中的“true”启用了其内部的LRU 特性。

3、利用LinkedHashMap的LRU特性,构造固定容量缓存系统

 

说明:上述代码中FixedSizeCahceMap构造参数中的fixSize指定了缓存的最大元素个数。removeEldestEntry方法重写了LinkedHashMap中关于判断是否删除“最久未用元素”的逻辑,“return size() > fixSize”表明,当缓存中的元素数达到fixSize时,将eldest元素淘汰掉。

4、LinkedHashMap实现源码分析

上文提到LinkedHashMap继承自HashMap,其主要目的是充分利用HashMap现有的<K,V>数据结构,在此基础上通过Override一些关键方法,将<K,V>结构与循环链表结构完美结合运用。被Override的几个关键点:

(1)定义链表的节点

(2)LinkedHashMap初始化时,建立循环链表

 

(3)向缓存中添加元素时的重写

(4)缓存命中时的方法重写

五、总结

LRU算法是缓存算法的入门必修课,而使用缓存算法也是很多互联网基础设施研发的必经之路,尤其在数据规模不断增大而物理资源相对有限的分布式领域,深入了解其机制原理并触类旁通,将是夯实高阶研发基础的第一步。

这篇关于剖析LRU算法及LinkedHashMap源码实现机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1141725

相关文章

Springboot处理跨域的实现方式(附Demo)

《Springboot处理跨域的实现方式(附Demo)》:本文主要介绍Springboot处理跨域的实现方式(附Demo),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不... 目录Springboot处理跨域的方式1. 基本知识2. @CrossOrigin3. 全局跨域设置4.

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

基于SpringBoot实现文件秒传功能

《基于SpringBoot实现文件秒传功能》在开发Web应用时,文件上传是一个常见需求,然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余,此时可以使用文件秒传技术通过识别重复... 目录前言文件秒传原理代码实现1. 创建项目基础结构2. 创建上传存储代码3. 创建Result类4.

SpringBoot日志配置SLF4J和Logback的方法实现

《SpringBoot日志配置SLF4J和Logback的方法实现》日志记录是不可或缺的一部分,本文主要介绍了SpringBoot日志配置SLF4J和Logback的方法实现,文中通过示例代码介绍的非... 目录一、前言二、案例一:初识日志三、案例二:使用Lombok输出日志四、案例三:配置Logback一

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

Python+PyQt5实现多屏幕协同播放功能

《Python+PyQt5实现多屏幕协同播放功能》在现代会议展示、数字广告、展览展示等场景中,多屏幕协同播放已成为刚需,下面我们就来看看如何利用Python和PyQt5开发一套功能强大的跨屏播控系统吧... 目录一、项目概述:突破传统播放限制二、核心技术解析2.1 多屏管理机制2.2 播放引擎设计2.3 专

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

java中反射(Reflection)机制举例详解

《java中反射(Reflection)机制举例详解》Java中的反射机制是指Java程序在运行期间可以获取到一个对象的全部信息,:本文主要介绍java中反射(Reflection)机制的相关资料... 目录一、什么是反射?二、反射的用途三、获取Class对象四、Class类型的对象使用场景1五、Class

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.