1. 程式人生 > 實用技巧 >快取LRU演算法——使用HashMap和雙向連結串列實現

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

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