LUR算法介紹
LRU(Least Recently Used),最近最少使用算法,從名字上可能不太好理解,我是這樣記的:LRU算法,淘汰最近一段時間內,最久沒有使用過的數據。
詳細的介紹可以參考百度百科:https://baike.baidu.com/item/LRU
實現LUR的原理
本文使用HashMap和雙向鏈表來實現LRU算法,原理如下圖所示:

其中:
1.雙向鏈表的主要功能是維護Node節點的順序;
2.HashMap的主要功能是存儲K-V緩存項,另外V為Node類型,也就是能通過key快速找到Node節點(快速定位到該Node節點在雙鏈表中的位置,而不用遍歷雙鏈表來找該Node節點);
比如我要刪除key為100的緩存項,那么根據HashMap的key快速找到100對應的node節點,然后在雙向鏈表中將節點進行刪除(修改前后Node的指針即可)。
當然可以將雙向鏈表替換為單鏈表(也能保存順序),但是這樣會有問題:
1.每次定位到要刪除的node后,都要從頭開始再遍歷一次鏈表,找到要刪除的節點的前一個節點,然后修改指針進行刪除節點操作;
2.每次查詢到key對應的value后,需要將對應的Node移動到第一個位置(表示最近訪問),那么有需要遍歷一遍鏈表,然后在修改指針將節點進行移動;
鑒於以上原因,所以不考慮使用單鏈表。
實現代碼
雙向鏈表節點
package cn.ganlixin.lru;
public class DLinkedNode {
public String key;
public String value;
public DLinkedNode pre;
public DLinkedNode next;
}
緩存類
package cn.ganlixin.lru;
import java.util.HashMap;
import java.util.Map;
/**
* 描述:使用Lru算法實現的cache
*
* @author ganlixin
* @link https://www.cnblogs.com/-beyond/p/13026406.html
* @create 2020-07-01
*/
public class LruCache {
/**
* 真正緩存數據的容器
*/
private Map<String, DLinkedNode> cache = new HashMap<>();
/**
* 當前緩存中的數據數量
*/
private int count;
/**
* 緩存的容量(最多能存多少個數據KV)
*/
private int capacity;
/**
* 雙向鏈表的頭尾節點(數據域key和value都為null)
*/
private DLinkedNode head, tail;
/**
* 唯一構造器,進行初始化
*
* @param capacity 最多能保存的緩存項數量
*/
public LruCache(int capacity) {
this.count = 0;
this.capacity = capacity;
this.head = new DLinkedNode();
this.tail = new DLinkedNode();
this.head.pre = null;
this.head.next = this.tail;
this.tail.pre = this.head;
this.tail.next = null;
}
/**
* 從緩存中獲取數據
*
* @param key 緩存中的key
* @return 緩存的value
*/
public String get(String key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return null;
}
// 每次訪問后,就需要將訪問的key對應的節點移到第一個位置(最近訪問)
moveToFirst(node);
return node.value;
}
/**
* 向緩存中添加數據
*
* @param key 元素key
* @param value 元素value
*/
public void set(String key, String value) {
// 先嘗試從緩存中獲取key對應緩存項(node)
DLinkedNode existNode = cache.get(key);
// key對應的數據不存在,則加入緩存
if (null == existNode) {
DLinkedNode newNode = new DLinkedNode();
newNode.key = key;
newNode.value = value;
// 放入緩存
cache.put(key, newNode);
// 將新加入的節點存入雙鏈表,且放到第一個位置
addNodeToFirst(newNode);
count++;
// 如果加入新的數據后,超過緩存容量,則要進行淘汰
if (count > capacity) {
DLinkedNode delNode = delLastNode();
cache.remove(delNode.key);
--count; // 淘汰后,數量建議
}
} else {
// key對應的數據已存在,則進行覆蓋
existNode.value = value;
// 將訪問的節點移動到第一個位置(最近訪問)
moveToFirst(existNode);
}
}
/**
* 添加新節點到雙向鏈表(新加入的節點位於第一個位置)
*
* @param newNode 新加入的節點
*/
private void addNodeToFirst(DLinkedNode newNode) {
newNode.next = head.next;
newNode.pre = head;
head.next.pre = newNode;
head.next = newNode;
}
/**
* 刪除雙向鏈表的尾節點(淘汰節點)
*
* @return 被刪除的節點
*/
private DLinkedNode delLastNode() {
DLinkedNode last = tail.pre;
delNode(last);
return last;
}
/**
* 將節點移動到雙向鏈表的第一個位置
*
* @param node 需要移動的節點
*/
private void moveToFirst(DLinkedNode node) {
// 將節點移動到頭部,有兩種方式:
delNode(node);
addNodeToFirst(node);
}
/**
* 刪除雙鏈表的節點(直接連接前后節點)
*
* @param node 要刪除的節點
*/
private void delNode(DLinkedNode node) {
DLinkedNode pre = node.pre;
DLinkedNode post = node.next;
pre.next = post;
post.pre = pre;
}
}
測試LRU cache
package cn.ganlixin.lru;
import org.junit.Test;
public class LruCacheTest {
@Test
public void test() {
LruCache cache = new LruCache(3);
cache.set("one", "111");
System.out.println(cache.get("one")); // 111
cache.set("two", "222");
System.out.println(cache.get("two")); // 222
cache.set("three", "333");
System.out.println(cache.get("three")); // 333
cache.set("four", "444");
System.out.println(cache.get("four")); // 444
System.out.println(cache.get("one")); //null
}
}
總結
上面實現的LRU cache存在很多問題:
1.只支持了get和set兩個操作,一般緩存還會支持其他操作,比如size、remove、expire、update....;
2.不支持並發修改,因為底層使用了HashMap,如果需要支持並發,可以修改為ConcurrentHashMap,同時對count、head、tail、capacity等屬性增加volatile關鍵字,各種修改接口(比如set、remove)增加鎖,以此來實現並發安全;
3.存儲開銷比較大,每一個緩存項(set的k-v)都會創建額外的數據,比如node、pre node、next node;
4.時間開銷也不小,get和set不僅會修改map,還會修改雙向鏈表(將操作的節點移到第一個位置)。
總之,上面只是模擬了一下LRU算法,大家可以根據自己的理解進行修改完善。
