LRU 的来源
LRU, Least Recently Used 近期最少使用算法, 常应用于缓存中的数据淘汰, 其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高“。
一般实现思路
仔细看下这个算法的定义: 近期最少使用算法,其实就是按照”近期最少使用”这个条件去淘汰相应的数据。
举个例子,内存的访问速度要远远大于硬盘,所以应该把经常使用的数据放内存,而不常用的数据放磁盘, 假如有一块内存只能存放100个数据,有个程序负责管理这块内存的数据,流程大致如下:
客户端 -> 请求某个数据 -> 程序处理 -> 内存中有数据 -> 直接返回数据
|-> 内存无数据 -> 读取磁盘 -> 存储到内存 -> 返回数据
这样,当不停的访问新的数据,内存迟早会满。
所以就需要一个算法,在内存满的时候,淘汰那些不常用的数据,空出空间存储新的数据,这时可以用LRU。
既然是淘汰最近最少使用的数据,姑且就可以理解为,当内存满了的那个时刻,内存中,哪些数据最后一次被访问的时间最小, 不表示哪些数据是冷数据,应该被移除。
假如每条数据有一个属性lasttime,用来记录被访问时刻的时间,这样,每一条数据都有一个最后访问时间, 当内存满的时候,遍历所有元素,删除最后访问时间最小的那个元素:
lasttime = current_time()
lastKey = null;
if list full:
foreach each item of list:
if item.lasttime < lasttime:
lasttime = item.lasttime
lastKey = item.key
然后把lastKey指向的那条数据删除,当然可以改进上面的算法,一次删除多条数据,提高性能。
这个算法是可行的,但是有一个很大的问题,慢! 试想一下,假如有一千万条数据,每次删除都需要找出访问时间最早的那些数据,这是很耗资源的操作, 时间复杂度是O(N),跟数据量成正比,数据量越大,性能越低。
按照这个思路的实现代码(非线程安全):
优雅的实现思路
上文提到的方法,最大的瓶颈就在于遍历淘汰数据,如何优化这个过程?
细想一下,淘汰的数据,是最后访问时间最早的,上文的思路是为每一条记录加一个lastime,是否有更好的方法, 能够迅速(O(1)复杂度下)找到要淘汰的数据,删除操作也是常量级,性能肯定要高很多。
其实稍微转变一下思路,假如操作的数据在一个链表里面,试想这样一个过程:
访问一条记录 -> 把链表head 指向当前记录 -> 当前记录中的next指向head之前指向的记录 -> 返回当前数据
每访问一次数据,都把最新的访问的数据放到了链表头部,那链表尾部的数据就是最近没有访问过的数据!! 当链表满了,从链表尾部开始往前删除指定数目的数据,就能在常数级时间内腾出空间!
当想到的这个思路的时候,第一感觉,太神奇了,第二感觉,领会到数据结构是如何优雅的解决一些看似复杂耗时的问题!
然后看下关键的代码(非线程安全),首先是记录entry,链表里面的node
public class Entry{
public Entry prev;
public Entry next;
public int value;
public int key;
public Entry(int value, int key, Entry prev, Entry next){
this.value = value;
this.key = key;
this.prev = prev;
this.next = next;
}
}
淘汰算法:
// invalid the least used entity
public void lru(){
// here is a way to improve performance,
// if we remove last 10% entries, then this function may called less.
// remove the last one
Entry entry = this.tail.prev;
this.tail.prev = entry.prev;
entry.prev.next = this.tail;
map.remove(entry.key);
this.length--;
}
这里只实现的了一次淘汰最后一条记录,当链表满的时候,每插入一条数据,都会调用这个方法删除最后的数据, 性能一般,假如一次删除链表的1%,或者固定数目的多条数据,就能很明显的减少lru方法调用!
当然一次具体淘汰多少数据需要根据具体使用场景来确定,删除太多,可能会引起缓存命中率降低,太少,淘汰操作调用过多,很难给出个经验值。
完整代码: LRUCache.java