LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。 —来源:百度百科
LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)。
LRU算法的描述: 设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:
set(key,value)
:将记录(key,value)插入该结构。当缓存满时,将最久未使用的数据置换掉。
get(key)
:返回key对应的value值。
实现:最朴素的思想就是用数组+时间戳的方式,不过这样做效率较低。因此,我们可以用双向链表(LinkedList)+哈希表(HashMap)实现(链表用来表示位置,哈希表用来存储和查找),在Java里有对应的数据结构LinkedHashMap
。
//设置容量为10
private static LRUCache<String, Integer> cache = new LRUCache<>(10);
public static void main(String[] args) {
//存入10个数据,之后cache满了
for (int i = 0; i < 10; i++) {
cache.put("Test" + i, i); // 键值对
}
System.out.println("目前所有缓存:" + cache);
cache.get("Test3");
System.out.println("拿了Test3后的缓存:" + cache);
cache.get("Test4");
System.out.println("拿了Test4后的缓存:" + cache);
cache.get("Test5");
System.out.println("拿了Test5后的缓存:" + cache);
cache.get("Test5");
System.out.println("再拿了Test5后的缓存:" + cache);
cache.put("Test" + 10, 10); //cache已经满了,则会将最长没用的删除
System.out.println("添加新元素10后的缓存(此时缓存已满):" + cache);
}
private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int cacheSize;
//使用指定的初始容量、装载因子和排序模式构造一个空的LinkedHashMap实例
//true表示访问顺序,false表示插入顺序
public LRUCache(int cacheSize) {
super(16, (float) 0.75, true);
this.cacheSize = cacheSize;
}
//如果不重写这个 会导致在新put数据时,不会删除最久没使用的,而是会新增到后边
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
//如果添加数据后,size()大于所要求的容量,则返回true,代表进行LRU删除
return size() > cacheSize; //size()返回此映射中的键值映射数
}
}
我们可以看到代码,在缓存满的情况下,再将Test10 put进缓存中,这将会使最近最久未使用的元素剔除,并且在继承LinkedHashMap时一定要重写它的removeEldestEntry()
方法,否则不会剔除,而是会将新元素加入到尾部。
结果如下:
目前所有缓存:{Test0=0, Test1=1, Test2=2, Test3=3, Test4=4, Test5=5, Test6=6, Test7=7, Test8=8, Test9=9}
拿了Test3后的缓存:{Test0=0, Test1=1, Test2=2, Test4=4, Test5=5, Test6=6, Test7=7, Test8=8, Test9=9, Test3=3}
拿了Test4后的缓存:{Test0=0, Test1=1, Test2=2, Test5=5, Test6=6, Test7=7, Test8=8, Test9=9, Test3=3, Test4=4}
拿了Test5后的缓存:{Test0=0, Test1=1, Test2=2, Test6=6, Test7=7, Test8=8, Test9=9, Test3=3, Test4=4, Test5=5}
再拿了Test5后的缓存:{Test0=0, Test1=1, Test2=2, Test6=6, Test7=7, Test8=8, Test9=9, Test3=3, Test4=4, Test5=5}
添加新元素10后的缓存(此时缓存已满):{Test1=1, Test2=2, Test6=6, Test7=7, Test8=8, Test9=9, Test3=3, Test4=4, Test5=5, Test10=10}
可以看到剔除了最久没使用的Test0,缓存大小也还是10。
这种方式虽然简单,在频繁访问热点数据的时候效率高,但是它的缺点在于如果是偶尔的批量访问不同的数据时其命中率就会很低。比如我频繁的访问A,接着访问不同的数据直到A被淘汰,此时我再访问A,则不得不又再次把A加入到Cache中,显然这种方式是不合时宜的,因为A已经访问了很多次了,不应该将其淘汰而把一堆只访问一次的数据加入到Cache中。有兴趣的同学还可以去了解一下:LRU-K
,2Q
,LFU
这些也是相关缓存算法,可以看一看,想了解LFU算法的可以参考一下这篇文章:网页链接
加油❤~