快取LRU演算法——使用HashMap和雙向連結串列實現
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節點);
比如我要刪除key為100的快取項,那麼根據HashMap的key快速找到100對應的node節點,然後在雙向連結串列中將節點進行刪除(修改指標即可)。
當然可以將雙向連結串列替換為單鏈表,但是這樣的話,每次定位到要刪除的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演算法,大家可以根據自己的理解進行修改完善。