LinkedHashMap实现的LRU缓存有什么局限性?业界有更好的实现方式吗?

简介: LinkedHashMap实现的LRU缓存有什么局限性?业界有更好的实现方式吗?

1. 前言


看了下文章发表记录,最近的一次原创发表记录还停留在2月份,大概有2个月没有写过文章了。有几个原因,其一是最近业务代码写的比较多,源码看得比较少,对于技术细节的思考没有那么多,值得写的东西也就少了很多。另一个原因是,随着我家小孩越来越大,我的业余时间分配给陪伴她学习的比重也越来越多,每天晚上要陪她认字和学英语,周末还要带她去上早教班。精力分散、惰性、技术思考不多,是最近断更的主要原因。这个公众号,我坚持了两年时间,付出了很多的努力,在收获了一些小小的成绩的同时,也得到了很多宝贵的技术积累以及技术提升,希望以后不会断更。


今天的主题是“聊一聊Glide缓存的技术细节”。此文的成因是,前日有个同学发了段Glide的源码给我看,希望我帮他解答一下他的疑惑。

glide的LruBitmapPool是根据宽高和Bitmap.Config来查找缓存中的Bitmap的,如果两张图片的宽高和Bitmap.Config相同,会不会出现取错图片的情况。

640.png


2. 初探LruBitmapPool原理


答案当然是显而易见的。glide这个大名鼎鼎且务实的官方库,经过了这么多年的迭代,当然不会在获取缓存的时候取错图片了。但是这个问题却是个很好的问题,glide用宽高作为key的做法引起了我极大的好奇心。虽然我之前没看过glide源码,但是LruBitmapPool的Lru让我倍感亲切。


看到Lru的第一反应是,LinkedHashMap嘛,心里旁白是"小样,这还不简单嘛?",于是定位到LruBitmapPool源码,Command+F快速的输入LinkedHashMap,按下回车键,信心满满的等待命中查询结果。结果却大吃一惊,用我女儿最近的口头禅就是,oops(哎哟),没有找到LinkedHashMap。这就更加激发了我的好奇心,跟着glide老大哥学习下不用LinkedHashMap是如何实现Lru缓存的。


当然,首先并不着急看源码,第一步,需要确认glide源码中的LRU缓存和用LinkedHashMap实现的LRU是不是具有同样的功能,把它的注释看一遍。

An BitmapPool implementation that uses an LruPoolStrategy to bucket Bitmaps and then uses an LRU eviction policy to evict Bitmaps from the least recently used bucket in order to keep the pool below a given maximum size limit.

好消息,glide的LRU缓存策略和用LinkedHashMap实现的LRU缓存策略是一样的,都是把最近最少使用的缓存对象置换出去。翻译如下:


BitmapPool使用LruPoolStrategy来存储Bitmaps,然后使用LRU驱逐策略从最近使用最少的桶中删除Bitmaps,以保证缓存池大小不会超过给定的最大尺寸限制。


好吧,顺藤摸瓜,查找LruPoolStrategy。

640.png

哇,熟悉的配方,熟悉的味道。这不就是自己实现了Map那一套么,虽然它没使用LinkedHashMap,但是终归没有逃出Map的技术思想。

640.png


LruPoolStrategy有三个实现类,AttributeStrategy、SizeStrategy、SizeConfigStrategy。顾名思义,这里使用的是策略模式,要搞明白这类代码,先搞懂最简单的那个实现类,即AttributeStrategy。为什么说它最简单,看源码注释,它是根据宽高和Bitmap.Config做精准匹配的,根据Key去查询缓存,只能命中宽高和Config相等的,当然这是Android4.4之前的Bitmap复用机制,谁简单,谁能降低研究问题的复杂度就先研究谁。

640.png


通读下源码,答案渐渐浮上水面了,有一个熟悉又陌生的东西,出现在眼前了,GroupedLinkedMap,说它熟悉,因为后面两个单词LinkedMap,顾名思义,虽然我不知道你具体是个啥,但你肯定是个Map或者类似Map的东西。说它陌生,因为GroupedLinkedMap我闻所未闻。欲知答案,老规矩,翻开源码,先看注释。


640.png


Similar to LinkedHashMap when access ordered except that it is access ordered on groups of bitmaps rather than individual objects. The idea is to be able to find the LRU bitmap size, rather than the LRU bitmap object. We can then remove bitmaps from the least recently used size of bitmap when we need to reduce our cache size.

For the purposes of the LRU, we count gets for a particular size of bitmap as an access, even if no bitmaps of that size are present. We do not count addition or removal of bitmaps as an access.

注释释放出以下几个信息。


  1. 它与LinkedHashMap相似,但又不完全相同,有以下几个区别。
  2. GroundLinkedMap是从宽高和Bitmap.Config的维度,将缓存分组。具体意思就是,如果有多张100X100尺寸的图片,它们会保存到该尺寸对应的桶里面。而传统的LinkedHashMap,如果保存多个Key一样的对象,则会发生替换。
  3. LinkedHashMap get、put、remove操作都会让内部的数据重新排序,而GroundLinkedMap则只有get操作会让内部的数据发生排序,我的理解是,这样更合理,put操作,是图片不用了才放到缓存中,至于后续会不会被用到,什么时候用,都不确定,所以应该降低它的新鲜度。
  4. GroundLinkedMap对于没命中到的Key,也会认为是一次有效的access。该key会排序放到最前面。


上面的注释,可谓高屋建瓴,提纲挈领,它与LinkedHashMap作对比,让开发者明白了GroundLinkedMap是干什么的,同时又让大家明白,为什么会摒弃LinkedHashMap另外造一个轮子。


GroundLinkedMap有两个属性,一个Map和一个双链表。Map是用来快速检索的,双链表是用来维护access顺序的,跟LinkedHashMap可谓异曲同工,区别上面也说了LinkedHashMap 的增、删、查操作,在命中缓存的情况下都会认为是一次有效access,会用双链表调整命中元素的顺序。而GroundLinkedMap则只针对查操作,不管命中与否,都会认为是一次有效access,会改变缓存的新鲜度。GroundLinkedMap的实现原理与LinkedHashMap实现原理极其相似,如果没看太懂,可以先自行搜索LinkedHashMap原理。


值得注意的是,LinkedEntry中的values正是缓存分组的容器。

640.png


3. LinkedHashMap和GroundLinkedMap区别


为了验证它们两的区别,同时为了让读者更直观的感受它们之间的区别,我写了一个测试代码。首先在GroundLinkedMap增加了一个遍历打印的方法。

640.png


然后写了一段测试代码,在get put remove 后,遍历两者的内容,观察它们顺序是否发生了变化。

640.png

640.png

结论:


  1. LinkedHashMap同一个key如果put多次,会发生替换而GroupedLinkedMap同一个key多次put,会分组。
  2. LinkedHashMap访问过的Entry会放到双链表的尾端而GroupedLinkedMap会将将访问过的Entry放到双链表的头部。(这只是区别,没有实际意义)
  3. 事实证明put操作不会影响GroupedLinkedMap内部双链表的数据顺序。看GroupedLinkedMap get(key2)和put(key3)之后的打印结果,顺序没变
  4. GroupedLinkedMap的get操作即使没有命中结果,也会把该key保存起来,并放到队首,看最后那一块的打印。
相关文章
|
1月前
|
缓存 算法 数据挖掘
深入理解缓存更新策略:从LRU到LFU
【10月更文挑战第7天】 在本文中,我们将探讨计算机系统中缓存机制的核心——缓存更新策略。缓存是提高数据检索速度的关键技术之一,无论是在硬件还是软件层面都扮演着重要角色。我们会详细介绍最常用的两种缓存算法:最近最少使用(LRU)和最少使用频率(LFU),并讨论它们的优缺点及适用场景。通过对比分析,旨在帮助读者更好地理解如何选择和实现适合自己需求的缓存策略,从而优化系统性能。
51 3
|
1月前
|
缓存 分布式计算 NoSQL
大数据-47 Redis 缓存过期 淘汰删除策略 LRU LFU 基础概念
大数据-47 Redis 缓存过期 淘汰删除策略 LRU LFU 基础概念
67 2
|
3月前
|
缓存 算法 前端开发
深入理解缓存淘汰策略:LRU和LFU算法的解析与应用
【8月更文挑战第25天】在计算机科学领域,高效管理资源对于提升系统性能至关重要。内存缓存作为一种加速数据读取的有效方法,其管理策略直接影响整体性能。本文重点介绍两种常用的缓存淘汰算法:LRU(最近最少使用)和LFU(最不经常使用)。LRU算法依据数据最近是否被访问来进行淘汰决策;而LFU算法则根据数据的访问频率做出判断。这两种算法各有特点,适用于不同的应用场景。通过深入分析这两种算法的原理、实现方式及适用场景,本文旨在帮助开发者更好地理解缓存管理机制,从而在实际应用中作出更合理的选择,有效提升系统性能和用户体验。
196 1
|
4月前
|
缓存 Python
在Python中,`functools`模块提供了一个非常有用的装饰器`lru_cache()`,它实现了最近最少使用(Least Recently Used, LRU)缓存策略。
在Python中,`functools`模块提供了一个非常有用的装饰器`lru_cache()`,它实现了最近最少使用(Least Recently Used, LRU)缓存策略。
|
3月前
|
存储 缓存 Java
|
3月前
|
存储 缓存 算法
Python 从零开始实现一个简单的LRU缓存
Python 从零开始实现一个简单的LRU缓存
42 0
|
4月前
|
缓存 算法 前端开发
前端 JS 经典:LRU 缓存算法
前端 JS 经典:LRU 缓存算法
98 0
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
77 6
|
14天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题