緩存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節點在雙鏈表中的位置,而不用遍歷雙鏈表來找該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算法,大家可以根據自己的理解進行修改完善。

 

  原文地址:https://www.cnblogs.com/-beyond/p/13026406.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM