簡介
LRU(Least Recently Used)直譯為“最近最少使用”。其實很多老外發明的詞直譯過來對於我們來說並不是特別好理解,甚至有些詞並不在國人的思維模式之內,比如快速排序中的Pivot,模擬信號中的Analog 等等。筆者認為最好的理解方式就是看他誕生的原因,看這個概念的出現如何一步一步演變為現在的樣子。假如說你自己對某個問題想到了一個解決辦法,於是你按照語義給他起了個名字,假如你直接將這個詞兒說給別人,不知道這個詞兒來歷的人大概很難理解。所以為了力求方便理解,下面我們先來看看LRU是什么,主要是為了解決什么問題。
其實LRU這個概念映射到現實生活中非常好理解,就好比說小明的衣櫃中有很多衣服,假設他的衣服都只能放在這個櫃子里,小明每過一陣子小明就會買新衣服,不久小明的衣櫃就放滿了衣服。這個小明想了個辦法,按照衣服上次穿的時間排序,丟掉最長時間沒有穿過那個。這就是LRU策略。
映射到計算機概念中,上述例子中小明的衣櫃就是內存,而小明的衣服就是緩存數據。我們的內存是有限的。所以當緩存數據在內存越來越多,以至於無法存放即將到來的新緩存數據時,就必須扔掉最不常用的緩存數據。所以對於LRU的抽象總結如下:
- 緩存的容量是有限的
- 當緩存容量不足以存放需要緩存的新數據時,必須丟掉最不常用的緩存數據
實現
理解了LRU的原理之后,想要將其轉換為代碼並不是一件很困難的事。我們訪問緩存通常使用一個字符串來定位緩存數據(事實上其他數據形式也沒有關系),這種場景我們反射性的想到HashMap。所以我們先來簡單的定義一下LRUCachce類。
public class LRUCache {
private HashMap<String, Object> map;
private int capacity;
public Object get(String key) {
return map.get(key);
}
public void set(String key, Object value) {
this.map.put(key, value);
}
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<String, Object>();
}
}
這樣我們僅僅是定義了一個容量有限的LRUCache,可以存取數據,但並沒有實現緩存容量不足時丟棄最不常用的數據的功能,而這件事做起來似乎顯得稍微麻煩一些,問題在於我們如何找到最久沒有用的緩存。
一個最容易想到的辦法是我們在給這個緩存數據加一個時間戳,每次get緩存時就更新時間戳,這樣找到最久沒有用的緩存數據問題就能夠解決,但與之而來的會有兩個新問題:
- 雖然使用時間戳可以找到最久沒用的數據,但我們最少的代價也要將這些緩存數據遍歷一遍,除非我們維持一個按照時間戳排好序的SortedList。
- 添加時間戳的方式為我們的數據帶來了麻煩,我們並不太好在緩存數據中添加時間戳的標識,這可能需要引入新的包含時間戳的包裝對象。
而且我們的需要只是找到最久沒用使用的緩存數據,並不需要精確的時間。添加時間戳的方式顯然沒有利用這一特性,這就使得這個辦法從邏輯上來講可能不是最好的。
然而辦法總是有的,我們可以維護一個鏈表,當數據每一次查詢就將數據放到鏈表的head,當有新數據添加時也放到head上。這樣鏈表的tail就是最久沒用使用的緩存數據,每次容量不足的時候就可以刪除tail,並將前一個元素設置為tail,顯然這是一個雙向鏈表結構,因此我們定義LRUNode如下:
class LRUNode {
String key;
Object value;
LRUNode prev;
LRUNode next;
public LRUNode(String key, Object value) {
this.key = key;
this.value = value;
}
}
而LRUCache的簡單實現最終如下:
public class LRUCache {
private HashMap<String, LRUNode> map;
private int capacity;
private LRUNode head;
private LRUNode tail;
public void set(String key, Object value) {
LRUNode node = map.get(key);
if (node != null) {
node = map.get(key);
node.value = value;
remove(node, false);
} else {
node = new LRUNode(key, value);
if (map.size() >= capacity) {
// 每次容量不足時先刪除最久未使用的元素
remove(tail, true);
}
map.put(key, node);
}
// 將剛添加的元素設置為head
setHead(node);
}
public Object get(String key) {
LRUNode node = map.get(key);
if (node != null) {
// 將剛操作的元素放到head
remove(node, false);
setHead(node);
return node.value;
}
return null;
}
private void setHead(LRUNode node) {
// 先從鏈表中刪除該元素
if (head != null) {
node.next = head;
head.prev = node;
}
head = node;
if (tail == null) {
tail = node;
}
}
// 從鏈表中刪除此Node,此時要注意該Node是head或者是tail的情形
private void remove(LRUNode node, boolean flag) {
if (node.prev != null) {
node.prev.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;
} else {
tail = node.prev;
}
node.next = null;
node.prev = null;
if (flag) {
map.remove(node.key);
}
}
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<String, LRUNode>();
}
}