LRU(Least Recently Used) Cache的运行机制,通俗点说,就是缓存最近使用的数据,并淘汰最久没有使用的数据。
LRU的核心思想是数据的时间局部性(Temporal Locality),即"一个被访问的数据,在不久之后很可能还会被再次访问"。把它反过来说就是LRU的实现方式:“最早读取的数据,它不再被使用的可能性比刚刚读取的数据大”。
但是面对周期性读取的数据,如果周期大于缓存容量,那缓存总是无效的。
实现LRU本身也是一道面试题。我们就来看看怎么设计和实现吧。
首先来看一下详细的需求,然后设计我们的LRU:
K
来表示键的类型,V
来表示值的类型。因此需要实现以下两个接口:
public V get(K key)
public void put(K key, V value)
下面我们还需要考虑一些细节问题:
分析完了,接下来开始准备代码。
首先我们要定义链表的节点,Node
一个队列结构需要有两个操作:添加到队列尾部(即最新的节点)和删除队列头部(即最旧的节点)。
单向链表中:
因此与双向链表头尾都可以做最新或最旧的节点不同,我们这里只能选择链表头部作为队列头部(最旧的节点),链表尾部作为队列尾部(即最新的节点)。那么我们操作链表的三个方法就变成了:
void linkLast (Node node)
添加一个节点到尾部
Node unlinkFirst() {
删除头部,并返回删除的节点
Node unlink(Node node)
删除一个非尾部节点,如果node是尾部则不需要更新。
因为我们是通过将给定节点的下一个节点的内容复制到给定节点,
来实现O(1)删除的,相当于我们对调了两个节点的内容。
所以我们还需要在map中更新这两个节点的key对应的引用。
因此我们让unlink方法返回下一个节点的引用,方便更新key
(也可以直接在unlink内更新map,这里因为考虑到模块化,
所以我们选择让与链表相关的方法只处理链表)
添加新的缓存(下面称为newNode)的步骤,每一步都是O(1):
更新已有的缓存(下面称为node)步骤,每一步也都是O(1):
import java.util.HashMap;
public class LRUCacheSinglyLinkedList<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 LRUCacheSinglyLinkedList(int capacity) {
this.capacity = capacity;
this.size = 0;
this.first = null;
this.last = null;
// 给一个初始值,避免频繁rehash
index = new HashMap<>(capacity*2);
}
public V get(K key) {
Node<K, V> node = index.get(key);
if (node == null) {
return null;
} else {
V value = node.value;
cacheExistingItem(node);
return 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) {
linkLast(node);
index.put(node.key, node);
}
private void removeOldest() {
//删除最旧的数据
Node<K, V> removedNode = unlinkFirst();
if (removedNode != null) {
index.remove(removedNode.key);
// 帮助GC
removedNode.key = null;
}
}
private void cacheExistingItem(Node<K, V> node) {
//如果节点本来就是最新的,就不用更新了
if (node == last) {
return;
}
/*
我们对调了node和next两个节点的内容后,unlink了next来实现O(1)删除,
但index中的键仍然映射的是对调以前的节点。所以我们还需要在index中更新他们。
e.g.
before swap:
address of "node" = a, node.key = 1, node.value = 11
address of "next" = b, next.key = 2, next.value = 22
in index, we have
1: a
2: b
after swap:
address of "node" = a, node.key = 2, node.value = 22
address of "next" = b, next.key = 1, next.value = 11
in index, we still have
1: a
2: b
*/
Node<K, V> next = unlink(node);
index.put(node.key, node);
appendLatest(next);
}
private void linkLast(Node<K, V> node) {
if (node == null) {
return;
}
if (last == null) {
assert first == null;
first = node;
} else {
last.next = node;
}
last = node;
size++;
}
private Node<K, V> unlinkFirst() {
if (first == null) {
return null;
}
Node<K, V> copyFirst = first;
first = first.next;
if (first == null) {
last = null;
}
//帮助GC
copyFirst.value = null;
//不清空copyLast.key,key要用来删除index里面的值
size--;
return copyFirst;
}
private Node<K, V> unlink(Node<K, V> node) {
//如果node是尾节点,这个方法不应被调用
if (node == null || node == last) {
return null;
}
Node<K, V> next = node.next;
K keyCopy = node.key;
V valueCopy = node.value;
node.key = next.key;
node.value = next.value;
node.next = next.next;
next.key = keyCopy;
next.value = valueCopy;
next.next = null;
if (next == last) {
last = node;
}
size--;
return next;
}
@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="+key+":"+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> next;
public Node(K key, V value, Node<K, V> prev, Node<K, V> next) {
this.key = key;
this.value = value;
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() {
LRUCacheSinglyLinkedList<Integer, Integer> cache = new LRUCacheSinglyLinkedList<>(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,百度百科