原理
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

实现1
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

1. 新数据插入到链表头部;
2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3. 当链表满的时候,将链表尾部的数据丢弃。
分析
【命中率】
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。
【复杂度】
实现简单。
【代价】
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

使用LinkedHashMap实现
     LinkedHashMap底层就是用的HashMap加双链表实现的,而且本身已经实现了按照访问顺序的存储。此外,LinkedHashMap中本身就实现了一个方法removeEldestEntry用于判断是否需要移除最不常读取的数,方法默认是直接返回false,不会移除元素,所以需要重写该方法。即当缓存满后就移除最不常用的数。

  1. public class LRUCache<K, V> extends LinkedHashMap<K, V> {
  2.  
  3. private static final long serialVersionUID = 1L;
  4.  
  5. //缓存大小
  6. private int cacheSize;
  7.  
  8. public LRUCache(int cacheSize) {
  9. //第三个参数true是关键
  10. super(10, 0.75f, true);
  11. this.cacheSize = cacheSize;
  12. }
  13.  
  14. /**
  15. * 缓存是否已满
  16. */
  17. @Override
  18. protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
  19. boolean r = size() > cacheSize;
  20. if (r) {
  21. System.out.println("清除缓存key:" + eldest.getKey());
  22. }
  23. return r;
  24. }
  25.  
  26. //测试
  27. public static void main(String[] args) {
  28. LRUCache<String, String> cache = new LRUCache<String, String>(5);
  29. cache.put("1", "1");
  30. cache.put("2", "2");
  31. cache.put("3", "3");
  32. cache.put("4", "4");
  33. cache.put("5", "5");
  34.  
  35. System.out.println("初始化:");
  36. System.out.println(cache.keySet());
  37. System.out.println("访问3:");
  38. cache.get("3");
  39. System.out.println(cache.keySet());
  40. System.out.println("访问2:");
  41. cache.get("2");
  42. System.out.println(cache.keySet());
  43. System.out.println("增加数据6,7:");
  44. cache.put("6", "6");
  45. cache.put("7", "7");
  46. System.out.println(cache.keySet());
  47. }

实现2

  1. LRUCache的链表+HashMap实现

  1. 传统意义的LRU算法是为每一个Cache对象设置一个计数器,每次Cache命中则给计数器+1,而Cache用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。
  2.  
  3. 它的弊端很明显,如果Cache的数量少,问题不会很大, 但是如果Cache的空间过大,达到10W或者100W以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。效率也就非常的慢了。它的原理: Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。上面说了这么多的理论, 下面用代码来实现一个LRU策略的缓存。非线程安全,若实现安全,则在响应的方法加锁。
  1. public class LRUCacheDemo<K, V> {
  2.  
  3. private int currentCacheSize;
  4. private int CacheCapcity;
  5. private HashMap<K, CacheNode> caches;
  6. private CacheNode first;
  7. private CacheNode last;
  8.  
  9. public LRUCacheDemo(int size) {
  10. currentCacheSize = 0;
  11. this.CacheCapcity = size;
  12. caches = new HashMap<>(size);
  13. }
  14.  
  15. public void put(K k, V v) {
  16. CacheNode node = caches.get(k);
  17. if (node == null) {
  18. if (caches.size() >= CacheCapcity) {
  19. caches.remove(last.key);
  20. removeLast();
  21. }
  22. node = new CacheNode();
  23. node.key = k;
  24. }
  25. node.value = v;
  26. moveToFirst(node);
  27. caches.put(k, node);
  28. }
  29.  
  30. public Object get(K k) {
  31. CacheNode node = caches.get(k);
  32. if (node == null) {
  33. return null;
  34. }
  35. moveToFirst(node);
  36. return node.value;
  37. }
  38.  
  39. public Object remove(K k) {
  40. CacheNode node = caches.get(k);
  41. if (node != null) {
  42. if (node.pre != null) {
  43. node.pre.next = node.next;
  44. }
  45. if (node.next != null) {
  46. node.next.pre = node.pre;
  47. }
  48. if (node == first) {
  49. first = node.next;
  50. }
  51. if (node == last) {
  52. last = node.pre;
  53. }
  54. }
  55. return caches.remove(k);
  56. }
  57.  
  58. public void clear() {
  59. first = null;
  60. last = null;
  61. caches.clear();
  62. }
  63.  
  64. private void moveToFirst(CacheNode node) {
  65. if (first == node) {
  66. return;
  67. }
  68. if (node.next != null) {
  69. node.next.pre = node.pre;
  70. }
  71. if (node.pre != null) {
  72. node.pre.next = node.next;
  73. }
  74. if (node == last) {
  75. last = last.pre;
  76. }
  77. if (first == null || last == null) {
  78. first = last = node;
  79. return;
  80. }
  81. node.next = first;
  82. first.pre = node;
  83. first = node;
  84. first.pre = null;
  85. }
  86.  
  87. private void removeLast() {
  88. if (last != null) {
  89. last = last.pre;
  90. if (last == null) {
  91. first = null;
  92. } else {
  93. last.next = null;
  94. }
  95. }
  96. }
  97.  
  98. @Override
  99. public String toString() {
  100. StringBuilder sb = new StringBuilder();
  101. CacheNode node = first;
  102. while (node != null) {
  103. sb.append(String.format("%s:%s ", node.key, node.value));
  104. node = node.next;
  105. }
  106. return sb.toString();
  107. }
  108.  
  109. class CacheNode {
  110. CacheNode pre;
  111. CacheNode next;
  112. Object key;
  113. Object value;
  114.  
  115. public CacheNode() {
  116. }
  117. }
  118.  
  119. public static void main(String[] args) {
  120. LRUCache<Integer, String> lru = new LRUCache<Integer, String>(3);
  121. lru.put(1, "a"); // 1:a
  122. System.out.println(lru.toString());
  123. lru.put(2, "b"); // 2:b 1:a
  124. System.out.println(lru.toString());
  125. lru.put(3, "c"); // 3:c 2:b 1:a
  126. System.out.println(lru.toString());
  127. lru.put(4, "d"); // 4:d 3:c 2:b
  128. System.out.println(lru.toString());
  129. lru.put(1, "aa"); // 1:aa 4:d 3:c
  130. System.out.println(lru.toString());
  131. lru.put(2, "bb"); // 2:bb 1:aa 4:d
  132. System.out.println(lru.toString());
  133. lru.put(5, "e"); // 5:e 2:bb 1:aa
  134. System.out.println(lru.toString());
  135. lru.get(1); // 1:aa 5:e 2:bb
  136. System.out.println(lru.toString());
  137. lru.remove(11); // 1:aa 5:e 2:bb
  138. System.out.println(lru.toString());
  139. lru.remove(1); //5:e 2:bb
  140. System.out.println(lru.toString());
  141. lru.put(1, "aaa"); //1:aaa 5:e 2:bb
  142. System.out.println(lru.toString());
  143. }
  144. }

扩展:

扩展
1.LRU-K
LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。
相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。
数据第一次被访问时,加入到历史访问列表,如果数据在访问历史列表中没有达到K次访问,则按照一定的规则(FIFO,LRU)淘汰;当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列中删除,将数据移到缓存队列中,并缓存数据,缓存队列重新按照时间排序;缓存数据队列中被再次访问后,重新排序,需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即“淘汰倒数K次访问离现在最久的数据”。
LRU-K具有LRU的优点,同时还能避免LRU的缺点,实际应用中LRU-2是综合最优的选择。由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多

本文参考整理于  https://blog.csdn.net/wangxilong1991/article/details/70172302 ,https://blog.csdn.net/elricboa/article/details/78847305,感谢原作者的精彩分享!!!

精进之路之lru的更多相关文章

  1. python精进之路1---基础数据类型

    python精进之路1---基本数据类型 python的基本数据类型如上图,重点需要掌握字符串.列表和字典. 一.int.float类型 int主要是用于整数类型计算,float主要用于小数. int ...

  2. ❤️【Android精进之路-01】定计划,重行动来学Android吧❤️

    您好,我是码农飞哥,感谢您阅读本文,欢迎一键三连哦. Android精进之路第一篇,确定安卓学习计划. 干货满满,建议收藏,需要用到时常看看.小伙伴们如有问题及需要,欢迎踊跃留言哦~ ~ ~. 前言 ...

  3. 《Go 精进之路》 读书笔记 (第一次更新)

    <Go 精进之路> 读书笔记.简要记录自己打五角星的部分,方便复习巩固.目前看到p120 Go 语言遵从的设计哲学为组合 垂直组合:类型嵌入,快速让一个类型复用其他类型已经实现的能力,实现 ...

  4. python精进之路 -- open函数

    下面是python中builtins文件里对open函数的定义,我将英文按照我的理解翻译成中文,方便以后查看. def open(file, mode='r', buffering=None, enc ...

  5. 精进之路之AQS及相关组件

    AQS ( AbstractQueuedSynchronizer)是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Sem ...

  6. 精进之路之CAS

    CAS (Compare And Swap) 即比较交换, 是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术,本文将深入的介绍CAS的原理. 其算法核心思想如下 执行函数: ...

  7. 精进之路之volatile

    volatile 首先了解下Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他 ...

  8. 精进之路之JMM

    JMM (Java Memory Model) java内存模型 Java内存模型的抽象 Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一 ...

  9. 精进之路之HashMap

    HashMap本质的核心就是“数组+链表”,数组对于访问速度很快,而链表的优势在于插入速度快,HashMap集二者于一身. 提到HashMap,我们不得不提各个版本对于HashMap的不同.本文中先从 ...

随机推荐

  1. [ Servlet / JSP ] J2EE Web Application 中的 JSESSIONID 是什么?

    JSESSIONID is a cookie in J2EE web application which is used in session tracking. Since HTTP is a st ...

  2. python 学习笔记 5 ----> dive into python 3

    字符串 文本:屏幕上显示的字符或者其他的记号 计算机认识的东西:位(bit)和字节(byte) 文本的本质:某种字符编码方式保存的内容. 字符编码:一种映射(显示的内容  ----> 内存.磁盘 ...

  3. Python自学:第三章 动手试一试 3-4、3-5

    # -*- coding: GBK -*- liebiao = ["zhang", "li", "wang", "zhou&quo ...

  4. classList用法

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. SWUST OJ(599)

    拉丁方阵 #include <iostream> #include <cstdlib> using namespace std; int main() { int n; cin ...

  6. PHP以xml形式获取POST数据

    <?php namespace Home\Controller; use Think\Controller; class UrlController extends Controller { / ...

  7. 给MS的意见

    2017-02-27 WPF的中文注释文档翻译得很烂.太多了,列举不过来. 这个是 System.Threading.Tasks.Task.Exception: 获取导致 System.Aggrega ...

  8. 最长绝对文件路径——算法面试刷题1(google),字符串处理,使用tree遍历dfs类似思路

    假设我们通过以下的方式用字符串来抽象我们的文件系统: 字符串"dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext"代表了: dir subdir1 su ...

  9. redis安装,windows,linux版本并部署服务

    一.使用场景         项目中采用数据库访问量过大或访问过于频繁,将会对数据库带来很大的压力.redis数据库是以非关系数据库的出现,后来redis的迭代版本支持了缓存数据.登录session状 ...

  10. 正则--test exec search match replace

    1:test 是正则对象的方法不是字符串的方法,使用例子:正则对象也就是那个设定好的模式对象 var str = "hello world!"; var result = /^he ...