LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是「如果数据最近被访问过,那么将来被访问的几率也更高」。
实现 1
最常见的实现是使用一个链表(LinkedHashMap)保存缓存数据,详细算法实现如下:
- 新数据插入到链表头部;
2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3. 当链表满的时候,将链表尾部的数据丢弃。
分析
【命中率】
当存在热点数据时,LRU 的效率很好,但偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降,缓存污染情况比较严重。
【复杂度】
实现简单。
【代价】
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。(LinkedList 查询慢、增删快)
代码
package com.zcq.algorithm.LRU;import java.util.ArrayList;import java.util.Collection;import java.util.LinkedHashMap;import java.util.Map;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/*** 类说明:利用 LinkedHashMap 实现简单的缓存, 必须实现 removeEldestEntry 方法,具体参见JDK文档** @param <K>* @param <V>*/public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {/*** 容量*/private final int maxCapacity;/*** 负载因子*/private static final float DEFAULT_LOAD_FACTOR = 0.75f;private final Lock lock = new ReentrantLock();public LRULinkedHashMap(int maxCapacity) {super(maxCapacity, DEFAULT_LOAD_FACTOR, true);this.maxCapacity = maxCapacity;}/*** 重写此方法来判断是否移除元素** @param eldest* @return*/@Overrideprotected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {return size() > maxCapacity;}@Overridepublic boolean containsKey(Object key) {lock.lock();try {return super.containsKey(key);} finally {lock.unlock();}}@Overridepublic V get(Object key) {lock.lock();try {return super.get(key);} finally {lock.unlock();}}@Overridepublic V put(K key, V value) {lock.lock();try {return super.put(key, value);} finally {lock.unlock();}}@Overridepublic int size() {lock.lock();try {return super.size();} finally {lock.unlock();}}@Overridepublic void clear() {lock.lock();try {super.clear();} finally {lock.unlock();}}public Collection<Map.Entry<K, V>> getAll() {lock.lock();try {return new ArrayList<Map.Entry<K, V>>(super.entrySet());} finally {lock.unlock();}}public static void main(String[] args) {Map map = new LRULinkedHashMap(3);map.put(1, "a");System.out.println(map.toString());map.put(2, "b");System.out.println(map.toString());map.put(3, "c");System.out.println(map.toString());map.put(4, "d");System.out.println(map.toString());map.put(1, "a");System.out.println(map.toString());map.put(2, "b");System.out.println(map.toString());}}
实现 2
LRUCache 的链表 + HashMap 实现
传统意义的 LRU 算法是为每一个 Cache 对象设置一个计数器,每次 Cache 命中则给计数器 + 1,而 Cache 用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。
它的弊端很明显,如果 Cache 的数量少,问题不会很大, 但是如果 Cache 的空间过大,达到 10W 或者 100W 以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。效率也就非常的慢了。
原理: 将 Cache 的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的 Cache 直接加到链表头中。(并且 Map 查询元素为 O(1))
这样,在多次进行 Cache 操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的 Cache。
当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。
上面说了这么多的理论, 下面用代码来实现一个 LRU 策略的缓存。
非线程安全,若实现安全,则在响应的方法加锁。
package com.zcq.algorithm.LRU;import java.util.HashMap;/*** LRUCache 的链表 + HashMap 实现** @param <K>* @param <V>*/public class LRUCache<K, V> {private int currentCacheSize;private int cacheCapacity;private HashMap<K, CacheNode> caches;private CacheNode first;private CacheNode last;public LRUCache(int size) {currentCacheSize = 0;this.cacheCapacity = size;caches = new HashMap<>(size);}public void put(K k, V v) {CacheNode node = caches.get(k);if (node == null) {if (caches.size() >= cacheCapacity) {caches.remove(last.key);removeLast();}node = new CacheNode();node.key = k;}node.value = v;moveToFirst(node);caches.put(k, node);}public Object get(K k) {CacheNode node = caches.get(k);if (node == null) {return null;}moveToFirst(node);return node.value;}public Object remove(K k) {CacheNode node = caches.get(k);if (node != null) {if (node.pre != null) {node.pre.next = node.next;}if (node.next != null) {node.next.pre = node.pre;}if (node == first) {first = node.next;}if (node == last) {last = node.pre;}}return caches.remove(k);}public void clear() {first = null;last = null;caches.clear();}private void moveToFirst(CacheNode node) {if (first == node) {return;}if (node.next != null) {node.next.pre = node.pre;}if (node.pre != null) {node.pre.next = node.next;}if (node == last) {last = last.pre;}if (first == null || last == null) {first = last = node;return;}node.next = first;first.pre = node;first = node;first.pre = null;}private void removeLast() {if (last != null) {last = last.pre;if (last == null) {first = null;} else {last.next = null;}}}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();CacheNode node = first;while (node != null) {sb.append(String.format("%s:%s ", node.key, node.value));node = node.next;}return sb.toString();}class CacheNode {CacheNode pre;CacheNode next;Object key;Object value;public CacheNode() {}}public static void main(String[] args) {LRUCache<Integer, String> lru = new LRUCache<>(3);lru.put(1, "a"); // 1:aSystem.out.println(lru.toString());lru.put(2, "b"); // 2:b 1:aSystem.out.println(lru.toString());lru.put(3, "c"); // 3:c 2:b 1:aSystem.out.println(lru.toString());lru.put(4, "d"); // 4:d 3:c 2:bSystem.out.println(lru.toString());lru.put(1, "aa"); // 1:aa 4:d 3:cSystem.out.println(lru.toString());lru.put(2, "bb"); // 2:bb 1:aa 4:dSystem.out.println(lru.toString());lru.put(5, "e"); // 5:e 2:bb 1:aaSystem.out.println(lru.toString());lru.get(1); // 1:aa 5:e 2:bbSystem.out.println(lru.toString());lru.remove(11); // 1:aa 5:e 2:bbSystem.out.println(lru.toString());lru.remove(1); //5:e 2:bbSystem.out.println(lru.toString());lru.put(1, "aaa"); //1:aaa 5:e 2:bbSystem.out.println(lru.toString());}}
