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算法,大家可以根據自己的理解進行修改完善。