理解LinkedHashMap实现LRU缓存

时间:2022-03-26 16:48:15

       工作里项目的代码有看到用hashmap做简单缓存的,一直没去细看,最近详细研究了这方面一下,发现好像这题还是面试的比较多的题,自己在这里做下总结。

       自己用google搜中文结果,博客园里这篇博客是排第一的http://www.cnblogs.com/lzrabbit/p/3734850.html,算比较详实,但像文章里3楼评论说的那样,实现Map接口并不能保证获得线程安全,很好验证,可以自己用多线程编码模拟同时很多个客户端去访问容器。然后文章的另一个缺陷还是在于没有深入探究为什么要使用LinkedHashMap来实现LRU缓存。

        然后自己又搜索了英文解决方案,感觉结合文章1文章2就可以深入理解为什么是使用LinkedHashMap来实现是最优最方便的方案。

        简单说下,文章1从三个层面来谈我们实现缓存的需求。

第一是缓存大小要有限度,不能无限的耗内存;

第二是查找操作尽可能快,最好是O(1);

第三是当缓存容量满了要选择Entry替换算法,在这里是LRU;

(Fixed size: The cache needs to have some bounds to limit memory usage.
    Fast access: The cache insert and lookup operations need to be fast preferably O(1) time.
    Entry replacement algorithm: When the cache is full, the less useful cache entries are purged from cache. The algorithm to replace these entries is Least Recently Used     (LRU) - or the cache entries which have not been accessed recently will be replaced.)

        接着讨论了用HashMap实现的不足,主要是HashMap可以接收一个初始的容量值,但当容器满了之后它会自动扩容,所以我们要实现LRU需要重写它的put方法然后在insert之前选择一条纪录remove。一个可行的方案是维护一个时间戳(timestamp),然后每次remove掉最老的一条纪录,但是搜索的代价是O(N)。

       所以我们需要维护一个排好序的列表,而排序的基础就是其中纪录条被访问的顺序。我们可以使用双端链表,当某条纪录被访问的时候,同时把这条纪录移到链表的尾部,然后当容器满了的时候我们移除链表头的那条纪录。如果采用的数据结构是ArrayList,那当我们移除一条纪录的时候其它的纪录都要被移动,而双端链表则没有这个问题。
至此,我们已经提出了一个insert和search都是O(1)代价的方案。

        幸运的是,JDK里原生的LinkedHashMap符合了我们的所有需求。实现的代码可参考博客园的文章。文章2里是用的HashMap加作者自己代码实现的简略版双端链表,可以让人更直观的理解为什么可以insert和search的代价都是O(1)。如果觉得双端队列代码比较难理解可以自己画画图,实现不行再推荐一个网站可以看到各种数据结构操作的动态过程,上一张截图

理解LinkedHashMap实现LRU缓存

       上面可选数据结构,左下角选操作比方search、insert,右边是代码参考,下面的进度条还可以一步一步走,非常清晰。