本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
详细介绍了LRU缓存的概念,以及Java中如何通过LinkedList快速实现LRU缓存。
1 LRU缓存的概念
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
很多的软件框架都在使用LRU缓存策略,比如Redis的LRU缓存淘汰策略。并且LRU实现比较的简单,最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
- 新数据插入到链表头/尾部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头/尾部;
- 指定LRU缓存的容量,当链表长度大于容量时,将链表头/尾部的数据丢弃。
2 LinkedHashMap与LRU缓存
我们的LinkedHashMap已经提供了基于访问顺序的迭代机制,最近被访问的节点在尾部,最远被访问的节点在头部.那么自然可以实现LRU缓存,当然它的实现和下面这两个方法有关。
3.1 afterNodeInsertion方法
在插入元素操作之后,不光会调用linkNodeLast方法,在成功插入节点的情况下,在最后还会调用afterNodeInsertion方法,并传递evict=true(构造器中插入节点是传递evict=false)。同样HashMap同样只提供一个空实现。
比如put方法,存在两个情况,一种是替换value,一种是插入新节点,如果替换value,那么肯定访问到了某个节点,此时调用afterNodeAccess,如果是插入新节点,那么肯定是调用afterNodeInsertion方法,这两个方法不可能同时调用!
在LinkedHashMap重写的实现中,当内部的removeEldestEntry()方法返回 true 时会移除最远最久未访问的节点,也就是链表首部节点 head。evict 只有在构建 Map 的时候才为 false,在单独调用方法时为 true。
这个方法在插入节点之后调用,明显是因为LUR缓存容量有限制,新插入节点之后有可能需要移除最远最久未访问的节点。
/**
* HashMap提供的空实现
*
* @param evict 构造器中传递false,单独调用方法传递true
*/
void afterNodeInsertion(boolean evict) {
}
/**
* LinkedHashMap重写的实现
*
* @param evict 构造器中传递false,单独调用方法传递true
*/
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K, V> first;
//如果evict为true,并且大链表头节点不为null,并且removeEldestEntry(first)方法返回true
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
//那么调用removeNode移除头节点,这一移除方法中具有afterNodeRemoval方法
removeNode(hash(key), key, null, false, true);
}
}
3.2 removeEldestEntry方法
我们看到afterNodeInsertion方法内部调用了removeEldestEntry方法并以返回值作为是否需要移除头节点的判断条件之一。
removeEldestEntry方法是LinkedHashMap 自己的方法,并且还是一个抽象方法。 默认返回false,如果需要让它返回true或者根据代码返回,需要继承 LinkedHashMap 并且覆盖这个方法的实现。
该方法在实现 LRU 的缓存中特别有用,在该方法中可以设置缓存容量,然后比较节点总数和缓存容量的大小,当节点总数超过缓存容量时可以返回true(因为新增节点成功之后会调用afterNodeInsertion方法),然后通过移除最近最久未使用的节点(头节点),从而保证缓存空间足够,并且缓存的数据都是热点数据。
/**
* 移除最近最少被访问条件之一,通过覆盖此抽象方法可实现不同策略的缓存
*
* @param eldest 大链表头节点
* @return true,移除 false,不移除
*/
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return false;
}
2 使用LinkedList实现LRU缓存
当我们基于 LinkedHashMap实现缓存时,通过继承LinkedHashMap并且覆写removeEldestEntry方法,再构造对象是设置accessOrder为true,可以实现自定义策略的 LRU 缓存。比如我们可以根据节点数量判断是否移除最近最少被访问的节点,或者根据节点的存活时间判断是否移除该节点等。
案例:
/**
* 简单的LRU缓存,通过继承LinkedHashMap来实现
*/
class LRUCache<K, V> extends LinkedHashMap<K, V> {
/**
* 缓存容量
*/
private int maxEntries;
/**
* 构造器
*
* @param maxEntries 最大容量
*/
LRUCache(Integer maxEntries) {
//调用父类构造器
super(maxEntries, 0.75f, true);
this.maxEntries = maxEntries;
}
/**
* 通过重写removeEldestEntry方法,加入一定的条件,满足条件返回true。
*
* @param eldest 大链表头节点
* @return true,表示允许移除头节点;false,表示不允许移除头节点
*/
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
//如果节点数量大于LRU缓存容量,那么返回true
return size() > maxEntries;
}
/**
* 测试
*/
public static void main(String[] args) {
//新建LRUCache,容量为5,首先循环存放十次
LRUCache<Integer, Integer> cache = new LRUCache<>(5);
for (int i = 0; i < 10; i++) {
cache.put(i, i * i);
}
System.out.println("调用10次插入方法后,缓存的内容======>");
System.out.println(cache + "\n");
System.out.println("访问键为7的节点后,缓存内容======>");
cache.get(7);
System.out.println(cache + "\n");
System.out.println("访问键为1的节点后,缓存内容======>");
cache.get(1);
System.out.println(cache + "\n");
System.out.println("插入键值为1的键值对后,缓存内容======>");
cache.put(1, 1);
System.out.println(cache);
System.out.println("删除键为6的键值对后,缓存内容:");
cache.remove(6);
System.out.println(cache);
System.out.println("插入键值为7的键值对后,缓存内容:");
cache.put(7, 7);
System.out.println(cache);
}
}
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!