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

实现 1

最常见的实现是使用一个链表(LinkedHashMap)保存缓存数据,详细算法实现如下:
缓存淘汰算法 LRU 算法实现 - 图1

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

分析
【命中率】
当存在热点数据时,LRU 的效率很好,但偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降,缓存污染情况比较严重。
【复杂度】
实现简单。
【代价】
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。(LinkedList 查询慢、增删快)

代码

  1. package com.zcq.algorithm.LRU;
  2. import java.util.ArrayList;
  3. import java.util.Collection;
  4. import java.util.LinkedHashMap;
  5. import java.util.Map;
  6. import java.util.concurrent.locks.Lock;
  7. import java.util.concurrent.locks.ReentrantLock;
  8. /**
  9. * 类说明:利用 LinkedHashMap 实现简单的缓存, 必须实现 removeEldestEntry 方法,具体参见JDK文档
  10. *
  11. * @param <K>
  12. * @param <V>
  13. */
  14. public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
  15. /**
  16. * 容量
  17. */
  18. private final int maxCapacity;
  19. /**
  20. * 负载因子
  21. */
  22. private static final float DEFAULT_LOAD_FACTOR = 0.75f;
  23. private final Lock lock = new ReentrantLock();
  24. public LRULinkedHashMap(int maxCapacity) {
  25. super(maxCapacity, DEFAULT_LOAD_FACTOR, true);
  26. this.maxCapacity = maxCapacity;
  27. }
  28. /**
  29. * 重写此方法来判断是否移除元素
  30. *
  31. * @param eldest
  32. * @return
  33. */
  34. @Override
  35. protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
  36. return size() > maxCapacity;
  37. }
  38. @Override
  39. public boolean containsKey(Object key) {
  40. lock.lock();
  41. try {
  42. return super.containsKey(key);
  43. } finally {
  44. lock.unlock();
  45. }
  46. }
  47. @Override
  48. public V get(Object key) {
  49. lock.lock();
  50. try {
  51. return super.get(key);
  52. } finally {
  53. lock.unlock();
  54. }
  55. }
  56. @Override
  57. public V put(K key, V value) {
  58. lock.lock();
  59. try {
  60. return super.put(key, value);
  61. } finally {
  62. lock.unlock();
  63. }
  64. }
  65. @Override
  66. public int size() {
  67. lock.lock();
  68. try {
  69. return super.size();
  70. } finally {
  71. lock.unlock();
  72. }
  73. }
  74. @Override
  75. public void clear() {
  76. lock.lock();
  77. try {
  78. super.clear();
  79. } finally {
  80. lock.unlock();
  81. }
  82. }
  83. public Collection<Map.Entry<K, V>> getAll() {
  84. lock.lock();
  85. try {
  86. return new ArrayList<Map.Entry<K, V>>(super.entrySet());
  87. } finally {
  88. lock.unlock();
  89. }
  90. }
  91. public static void main(String[] args) {
  92. Map map = new LRULinkedHashMap(3);
  93. map.put(1, "a");
  94. System.out.println(map.toString());
  95. map.put(2, "b");
  96. System.out.println(map.toString());
  97. map.put(3, "c");
  98. System.out.println(map.toString());
  99. map.put(4, "d");
  100. System.out.println(map.toString());
  101. map.put(1, "a");
  102. System.out.println(map.toString());
  103. map.put(2, "b");
  104. System.out.println(map.toString());
  105. }
  106. }

实现 2

LRUCache 的链表 + HashMap 实现
缓存淘汰算法 LRU 算法实现 - 图2
传统意义的 LRU 算法是为每一个 Cache 对象设置一个计数器,每次 Cache 命中则给计数器 + 1,而 Cache 用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。

它的弊端很明显,如果 Cache 的数量少,问题不会很大, 但是如果 Cache 的空间过大,达到 10W 或者 100W 以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。效率也就非常的慢了。

原理: 将 Cache 的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的 Cache 直接加到链表头中。(并且 Map 查询元素为 O(1))

这样,在多次进行 Cache 操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的 Cache。

当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。

上面说了这么多的理论, 下面用代码来实现一个 LRU 策略的缓存。

非线程安全,若实现安全,则在响应的方法加锁。

  1. package com.zcq.algorithm.LRU;
  2. import java.util.HashMap;
  3. /**
  4. * LRUCache 的链表 + HashMap 实现
  5. *
  6. * @param <K>
  7. * @param <V>
  8. */
  9. public class LRUCache<K, V> {
  10. private int currentCacheSize;
  11. private int cacheCapacity;
  12. private HashMap<K, CacheNode> caches;
  13. private CacheNode first;
  14. private CacheNode last;
  15. public LRUCache(int size) {
  16. currentCacheSize = 0;
  17. this.cacheCapacity = size;
  18. caches = new HashMap<>(size);
  19. }
  20. public void put(K k, V v) {
  21. CacheNode node = caches.get(k);
  22. if (node == null) {
  23. if (caches.size() >= cacheCapacity) {
  24. caches.remove(last.key);
  25. removeLast();
  26. }
  27. node = new CacheNode();
  28. node.key = k;
  29. }
  30. node.value = v;
  31. moveToFirst(node);
  32. caches.put(k, node);
  33. }
  34. public Object get(K k) {
  35. CacheNode node = caches.get(k);
  36. if (node == null) {
  37. return null;
  38. }
  39. moveToFirst(node);
  40. return node.value;
  41. }
  42. public Object remove(K k) {
  43. CacheNode node = caches.get(k);
  44. if (node != null) {
  45. if (node.pre != null) {
  46. node.pre.next = node.next;
  47. }
  48. if (node.next != null) {
  49. node.next.pre = node.pre;
  50. }
  51. if (node == first) {
  52. first = node.next;
  53. }
  54. if (node == last) {
  55. last = node.pre;
  56. }
  57. }
  58. return caches.remove(k);
  59. }
  60. public void clear() {
  61. first = null;
  62. last = null;
  63. caches.clear();
  64. }
  65. private void moveToFirst(CacheNode node) {
  66. if (first == node) {
  67. return;
  68. }
  69. if (node.next != null) {
  70. node.next.pre = node.pre;
  71. }
  72. if (node.pre != null) {
  73. node.pre.next = node.next;
  74. }
  75. if (node == last) {
  76. last = last.pre;
  77. }
  78. if (first == null || last == null) {
  79. first = last = node;
  80. return;
  81. }
  82. node.next = first;
  83. first.pre = node;
  84. first = node;
  85. first.pre = null;
  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. @Override
  98. public String toString() {
  99. StringBuilder sb = new StringBuilder();
  100. CacheNode node = first;
  101. while (node != null) {
  102. sb.append(String.format("%s:%s ", node.key, node.value));
  103. node = node.next;
  104. }
  105. return sb.toString();
  106. }
  107. class CacheNode {
  108. CacheNode pre;
  109. CacheNode next;
  110. Object key;
  111. Object value;
  112. public CacheNode() {
  113. }
  114. }
  115. public static void main(String[] args) {
  116. LRUCache<Integer, String> lru = new LRUCache<>(3);
  117. lru.put(1, "a"); // 1:a
  118. System.out.println(lru.toString());
  119. lru.put(2, "b"); // 2:b 1:a
  120. System.out.println(lru.toString());
  121. lru.put(3, "c"); // 3:c 2:b 1:a
  122. System.out.println(lru.toString());
  123. lru.put(4, "d"); // 4:d 3:c 2:b
  124. System.out.println(lru.toString());
  125. lru.put(1, "aa"); // 1:aa 4:d 3:c
  126. System.out.println(lru.toString());
  127. lru.put(2, "bb"); // 2:bb 1:aa 4:d
  128. System.out.println(lru.toString());
  129. lru.put(5, "e"); // 5:e 2:bb 1:aa
  130. System.out.println(lru.toString());
  131. lru.get(1); // 1:aa 5:e 2:bb
  132. System.out.println(lru.toString());
  133. lru.remove(11); // 1:aa 5:e 2:bb
  134. System.out.println(lru.toString());
  135. lru.remove(1); //5:e 2:bb
  136. System.out.println(lru.toString());
  137. lru.put(1, "aaa"); //1:aaa 5:e 2:bb
  138. System.out.println(lru.toString());
  139. }
  140. }