LRU(Least Recently Used) Cache的运行机制,通俗点说,就是缓存最近使用的数据,并淘汰最久没有使用的数据。
LRU的核心思想是数据的时间局部性(Temporal Locality),即"一个被访问的数据,在不久之后很可能还会被再次访问"。把它反过来说就是LRU的实现方式:“最早读取的数据,它不再被使用的可能性比刚刚读取的数据大”。
但是面对周期性读取的数据,如果周期大于缓存容量,那缓存总是无效的。
- e.g. 假如周期性的读取五个数据:ABCDE,而缓存大小只有3,那么缓存总是不会被命中。
实现LRU本身也是一道面试题。我们就来看看怎么设计和实现吧。
分析与设计
首先来看一下详细的需求,然后设计我们的LRU:
- 缓存使用键值对储存,我们使用泛型
K
来表示键的类型,V
来表示值的类型。因此需要实现以下两个接口:public V get(K key)
public void put(K key, V value)
- 缓存需要设置一个容量。超出容量后会淘汰最长时间没有访问的数据
- 淘汰最长时间没有访问的数据,也就是先进入缓存的先被删除,我们很自然地就可以联想到队列。
- 当缓存中某个数据被使用时,我们要将这个数据从队列中拿出来放在队列的尾部。而且LRU在每次使用时都要更新使用次序的信息,因此增删数据的时间复杂度对缓存性能至关重要。如此一来,问题就变成了:“用什么数据结构能在最短的时间内增删数元素?”。最短那肯定要从O(1)时间开始思考,而我们正好熟知这样的一种数据结构:链表
- 同时我们还需要解决怎么尽快从缓存中找到我们的数据。如果仅仅使用链表,我们需要O(n)的时间才能找到想要的元素。而基本数据结构中,能做到O(1)时间根据键查找元素的,就是哈希表了。
- 结论是:我们使用链表储存实际的数据,来实现缓存队列,然后用哈希表作为数据的索引帮助实现O(1)查找。
下面我们还需要考虑一些细节问题:
- 如果想要在O(1)时间内获得尾部节点,或往链表尾部添加节点,我们必须维持一个尾部的引用。
- 使用单向链表还是双向链表?都可以,但单向链表有限制。
- 使用双向链表,在删除节点时可以直接连接前后两个节点,更加方便
- 使用单向链表则稍微复杂一些,我专门写了一篇,欢迎捧场和指教:LRU缓存单向链表O(1)读写,JAVA实现详解
代码实现(JAVA 1.8)
分析完了,接下来开始准备代码。
首先我们要定义链表的节点,Node<K, V>
- K:key的类型, V:value的类型
- 在节点中储存key是为了在删除的时候,能通过节点中储存的key值,在map中使用O(1)时间查找到对应的链表节点
我们选择链表头部作为最新使用的缓存(即队列尾部),链表尾部为最旧的(即队列尾部)。当然,对于双向链表,不管链表头还是尾作队列尾部都可以。根据这个设计,我们需要为链表添加以下几个方法:
void linkFirst(Node<K, V> node)
添加一个节点到头部
Node<K, V> unlinkLast() {
删除尾部,并返回删除的节点
void unlink(Node<K, V> node)
从链表中间任意删除一个节点
添加新的缓存(下面称为newNode)的步骤,每一步都是O(1):
- 将newNode加入队列尾部,作为最新的缓存
- 在map中添加键值对:newNode.key=newNode
- 检查是否超过缓存容量,如果超过,删除最旧的缓存
更新已有的缓存(下面称为node)步骤,每一步也都是O(1):
- 根据key值在map中找到node
- 将node从队列中移除
- 将node加入队列尾部,作为最新的缓存
双向链表的具体操作可以参考LinkedList的源码。
接下来贴出代码:
import java.util.HashMap;
public class LRUCacheDoublyLinkedList<K, V> {
private int capacity;
private int size;
private HashMap<K, Node<K, V>> index;
/**
* 最旧值
*/
private Node<K, V> first;
/**
* 最旧值
*/
private Node<K, V> last;
public LRUCacheDoublyLinkedList(int capacity) {
this.capacity = capacity;
this.size = 0;
this.first = null;
this.last = null;
//设置一个与capacity接近的初始值,减少rehash次数
index = new HashMap<>(capacity*2);
}
public V get(K key) {
Node<K, V> node = index.get(key);
if (node == null) {
return null;
} else {
cacheExistingItem(node);
return node.value;
}
}
public void put(K key, V value) {
Node<K, V> node = index.get(key);
//缓存一个新的值
if (node == null) {
node = new Node<>(key, value, last, null);
appendLatest(node);
if (size > capacity) {
removeOldest();
}
} else {
node.value = value;
cacheExistingItem(node);
}
}
private void appendLatest(Node<K, V> node) {
linkFirst(node);
index.put(node.key, node);
}
private void removeOldest() {
//删除最旧的数据
Node<K, V> removedNode = unlinkLast();
if (removedNode != null) {
index.remove(removedNode.key);
// 帮助GC
removedNode.key = null;
}
}
private void cacheExistingItem(Node<K, V> node) {
unlink(node);
linkFirst(node);
}
private void linkFirst(Node<K, V> node) {
if (node == null) {
return;
}
if (first == null) {
assert last == null;
last = node;
} else {
first.prev = node;
}
node.prev = null;
node.next = first;
first = node;
size++;
}
private Node<K, V> unlinkLast() {
if (last == null) {
return null;
}
Node<K, V> copyLast = last;
last = last.prev;
if (last == null) {
first = null;
} else {
last.next = null;
}
//帮助GC
copyLast.value = null;
//不清空copyLast.key,key要用来删除index里面的值
copyLast.prev = null;
size--;
return copyLast;
}
private void unlink(Node<K, V> node) {
if (node == null) {
return;
}
Node<K, V> prev = node.prev;
Node<K, V> next = node.next;
if (prev == null) {
first = next;
} else {
assert node != first;
prev.next = next;
}
if (next == null) {
last = prev;
} else {
assert node != last;
next.prev = prev;
}
node.prev = null;
node.next = null;
size--;
}
@Override
public String toString() {
StringBuffer buff = new StringBuffer("LRUCache{" +
"capacity=" + capacity +
", size=" + size +
", elements=["
);
Node<K, V> node = first;
while (node != null) {
buff.append(node.toString());
buff.append(", ");
node = node.next;
}
buff.append("]}");
return buff.toString();
}
public void showIndexStructure() {
StringBuffer buff = new StringBuffer('{');
for (K key : index.keySet()) {
buff.append("{entry="+index.get(key).toString()+", ");
buff.append("}, \n");
}
System.out.println(buff.toString());
}
private static class Node<K, V> {
/**
* 用于在O(1)时间更新index
*/
K key;
/**
* 缓存的内容
*/
V value;
Node<K, V> prev;
Node<K, V> next;
public Node(K key, V value, Node<K, V> prev, Node<K, V> next) {
this.key = key;
this.value = value;
this.prev = prev;
this.next = next;
}
@Override
public String toString() {
return "Node{"+ key +
": " + value +
'}';
}
}
}
测试类:
import org.junit.Assert;
import org.junit.Test;
public class TestLRUCache {
@Test
public void testDoublyLinkedLRUCache() {
LRUCacheDoublyLinkedList<Integer, Integer> cache = new LRUCacheDoublyLinkedList<>(3);
System.out.println(cache);
Assert.assertEquals(null, cache.get(5));
cache.put(5, 55);
cache.put(5, 55);
System.out.println(cache);
cache.put(4, 44);
System.out.println(cache);
cache.put(10, 0);
System.out.println(cache);
cache.put(2, 22);
System.out.println(cache);
cache.showIndexStructure();
Assert.assertEquals(Integer.valueOf(44), cache.get(4));
System.out.println(cache);
cache.showIndexStructure();
}
}
码字不易,觉得有帮助就给我点个赞吧!我会继续努力的!
Reference:
局部性原理,百度百科
LRU,百度百科