LinkedHashMap(實現LRU緩存)


LinkedHashMap內部維護了一個雙向鏈表,能保證元素按插入的順序訪問,也能以訪問順序訪問。

底層通過LinkedList+HashMap實現

關鍵屬性:

/**
* 雙向鏈表頭節點
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 雙向鏈表尾節點
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 是否按訪問順序排序,默認false,只是按照插入的順序排序,true,會根據訪問的順序排序(訪問肯定也包括插入啊)
*/
final boolean accessOrder;

最近訪問最近插入的都放在尾部,通過afterNodeAccess(Node<K,V> e)方法

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // 如果accessOrder為true,並且訪問的節點不是尾節點
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 把p節點從雙向鏈表中移除
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;

        if (a != null)
            a.before = b;
        else
            last = b;

        // 把p節點放到雙向鏈表的末尾
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        // 尾節點等於p
        tail = p;
        ++modCount;
    }
}

LPU(Least Recently Used):最近最少使用:

如果一個數據在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數據時,應當把最近最少使用的數據淘汰。

使用LinkedHashMap實現
     LinkedHashMap底層就是用的HashMap加雙鏈表實現的,而且本身已經實現了按照訪問順序的存儲。此外,LinkedHashMap中本身就實現了一個方法主要是利用他的這個方法,removeEldestEntry用於判斷是否需要移除最不常讀取的數,方法默認是直接返回false,不會移除元素,所以需要重寫該方法。即當緩存滿后就移除最不常用的數。

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_CACHE_SIZE = 100;

    private int limit;

    public LRUCache() {
        this(MAX_CACHE_SIZE);
    }

    public LRUCache(int cacheSize) {
        super(cacheSize, 0.75f, true);//這里是true喲,所以訪問的話也會被更新到末尾 this.limit = cacheSize;
    }

    public V save(K key, V val) {
        return put(key, val);
    }

    public V getOne(K key) {
        return get(key);
    }

    public boolean exists(K key) {
        return containsKey(key);
    }

    /**
     * 判斷節點數是否超限
     * @param eldest
     * @return 超限返回 true,否則返回 false
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > limit;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<K, V> entry : entrySet()) {
            sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
        }
        return sb.toString();
    }

    public static void main(String[] args){
        LRUCache<String, Integer> cache = new LRUCache<>(3);

        for (int i = 0; i < 10; i++) {
            cache.save("I" + i, i * i);
        }

        System.out.println("插入10個鍵值對后,緩存內容為:");
        System.out.println(cache + "\n");

        System.out.println("訪問鍵值為I8的節點后,緩存內容為:");
        cache.getOne("I8");
        System.out.println(cache + "\n");

        System.out.println("插入鍵值為I1的鍵值對后,緩存內容:");
        cache.save("I1", 1);
        System.out.println(cache);
    }
}

輸出:
插入10個鍵值對后,緩存內容為: I7:49 I8:64 I9:81 訪問鍵值為I8的節點后,緩存內容為: I7:49 I9:81 I8:64 插入鍵值為I1的鍵值對后,緩存內容: I9:81 I8:64 I1:1

參考:

https://www.imooc.com/article/67024

http://cmsblogs.com/?p=4733

擴展:

1.LRU-K

              LRU-K中的K代表最近使用的次數,因此LRU可以認為是LRU-1。LRU-K的主要目的是為了解決LRU算法“緩存污染”的問題,其核心思想是將“最近使用過1次”的判斷標准擴展為“最近使用過K次”。
相比LRU,LRU-K需要多維護一個隊列,用於記錄所有緩存數據被訪問的歷史。只有當數據的訪問次數達到K次的時候,才將數據放入緩存。當需要淘汰數據時,LRU-K會淘汰第K次訪問時間距當前時間最大的數據。數據第一次被訪問時,加入到歷史訪問列表,如果在訪問歷史列表中沒有達到K次訪問,則按照一定的規則(FIFO,LRU)淘汰;當訪問歷史隊列中的數據訪問次數達到K次后,將數據索引從歷史隊列中刪除,將數據移到緩存隊列中,並緩存數據,緩存隊列重新按照時間排序;緩存數據隊列中被再次訪問后,重新排序,需要淘汰數據時,淘汰緩存隊列中排在末尾的數據,即“淘汰倒數K次訪問離現在最久的數據”。LRU-K具有LRU的優點,同時還能避免LRU的缺點,實際應用中LRU-2是綜合最優的選擇。由於LRU-K還需要記錄那些被訪問過、但還沒有放入緩存的對象,因此內存消耗會比LRU要多。

2.two queue
              Two queues(以下使用2Q代替)算法類似於LRU-2,不同點在於2Q將LRU-2算法中的訪問歷史隊列(注意這不是緩存數據的)改為一個FIFO緩存隊列,即:2Q算法有兩個緩存隊列,一個是FIFO隊列,一個是LRU隊列。當數據第一次訪問時,2Q算法將數據緩存在FIFO隊列里面,當數據第二次被訪問時,則將數據從FIFO隊列移到LRU隊列里面,兩個隊列各自按照自己的方法淘汰數據。新訪問的數據插入到FIFO隊列中,如果數據在FIFO隊列中一直沒有被再次訪問,則最終按照FIFO規則淘汰;如果數據在FIFO隊列中再次被訪問到,則將數據移到LRU隊列頭部,如果數據在LRU隊列中再次被訪問,則將數據移動LRU隊列頭部,LRU隊列淘汰末尾的數據。

3.Multi Queue(MQ)
              MQ算法根據訪問頻率將數據划分為多個隊列,不同的隊列具有不同的訪問優先級,其核心思想是:優先緩存訪問次數多的數據。詳細的算法結構圖如下,Q0,Q1....Qk代表不同的優先級隊列,Q-history代表從緩存中淘汰數據,但記錄了數據的索引和引用次數的隊列:新插入的數據放入Q0,每個隊列按照LRU進行管理,當數據的訪問次數達到一定次數,需要提升優先級時,將數據從當前隊列中刪除,加入到高一級隊列的頭部;為了防止高優先級數據永遠不會被淘汰,當數據在指定的時間里沒有被訪問時,需要降低優先級,將數據從當前隊列刪除,加入到低一級的隊列頭部;需要淘汰數據時,從最低一級隊列開始按照LRU淘汰,每個隊列淘汰數據時,將數據從緩存中刪除,將數據索引加入Q-history頭部。如果數據在Q-history中被重新訪問,則重新計算其優先級,移到目標隊列頭部。Q-history按照LRU淘汰數據的索引。

參考:

https://blog.csdn.net/elricboa/article/details/78847305

除了可以用java自帶的集合類LInkedHashMap來實現,我們也可以自己寫一個,但基本原理和LinkedHashMap一樣,底層采用Hashmap,加上一個隊列:

基本需求:

  • 實現一個 LRU 緩存,當緩存數據達到 N 之后需要淘汰掉最近最少使用的數據。
  • N 小時之內沒有被訪問的數據也需要淘汰掉。(所以我們需要開一個守護線程,不斷的去輪詢判斷隊列頭是否過期了)
public class LRUAbstractMap extends java.util.AbstractMap {
    /**
     * 檢查是否超期線程
     */
    private ExecutorService checkTimePool ;

    /**
     * map 最大size
     */
    private final static int MAX_SIZE = 1024 ;
    //用一個阻塞隊列存儲
    private final static ArrayBlockingQueue<Node> QUEUE = new ArrayBlockingQueue<>(MAX_SIZE) ;
    private int arraySize ;
    /**
     * 數組
     */
    private Object[] arrays ;//這個數組有點像hashmap
    /**
     * 超時時間
     */
    private final static Long EXPIRE_TIME = 60 * 60 * 1000L ;

    /**
     * 整個 Map 的大小
     */
    private volatile AtomicInteger size  ;


    public LRUAbstractMap() {
        arraySize = 1024;
        arrays = new Object[arraySize] ;
        //開啟一個線程檢查最先放入隊列的值是否超期
        executeCheckTime();
    }

    /**
     * 開啟一個線程檢查最先放入隊列的值是否超期 設置為守護線程
     */
    private void executeCheckTime() {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("check-thread-%d")
                .setDaemon(true)
                .build();
        checkTimePool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(1),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
        checkTimePool.execute(new CheckTimeThread()) ;

    }

    @Override
    public Object put(Object key, Object value) {
        int hash = hash(key);
        int index = hash % arraySize ;
        Node currentNode = (Node) arrays[index] ;
        if (currentNode == null){
            arrays[index] = new Node(null,null, key, value);
            //寫入隊列
            QUEUE.offer((Node) arrays[index]) ;
        }else {
            Node cNode = currentNode ;
            Node nNode = cNode ;
            //存在就覆蓋
            if (nNode.key == key){
                cNode.val = value ;
            }
            while (nNode.next != null){
                //key 存在 就覆蓋 簡單判斷
                if (nNode.key == key){
                    nNode.val = value ;//去更新隊列和Hashmap里面的值
                    break ;
                }else {
                    //不存在就新增鏈表
                    Node node = new Node(nNode,null,key,value) ;
                    //寫入隊列
                    QUEUE.offer(currentNode) ;
                    cNode.next = node ;
                }

                nNode = nNode.next ;
            }

        }
        return null ;
    }


    @Override
    public Object get(Object key) {

        int hash = hash(key) ;
        int index = hash % arraySize ;
        Node currentNode = (Node) arrays[index] ;

        if (currentNode == null){
            return null ;
        }
        if (currentNode.next == null){
            //更新時間
            currentNode.setUpdateTime(System.currentTimeMillis());
            //沒有沖突
            return currentNode ;

        }
        Node nNode = currentNode ;
        while (nNode.next != null){
            if (nNode.key == key){
                //更新時間
                currentNode.setUpdateTime(System.currentTimeMillis());
                return nNode ;
            }

            nNode = nNode.next ;
        }

        return super.get(key);
    }


    @Override
    public Object remove(Object key) {

        int hash = hash(key) ;
        int index = hash % arraySize ;
        Node currentNode = (Node) arrays[index] ;

        if (currentNode == null){
            return null ;
        }

        if (currentNode.key == key){
            arrays[index] = null ;
            //移除隊列
            QUEUE.poll();
            return currentNode ;
        }

        Node nNode = currentNode ;
        while (nNode.next != null){
            if (nNode.key == key){
                //在鏈表中找到了 把上一個節點的 next 指向當前節點的下一個節點
                nNode.pre.next = nNode.next ;
                nNode = null ;

                //移除隊列
                QUEUE.poll();

                return nNode;
            }

            nNode = nNode.next ;
        }

        return super.remove(key);
    }

    /**
     * 增加size


    /**
     * 鏈表
     */
    private class Node{
        private Node next ;
        private Node pre ;
        private Object key ;
        private Object val ;
        private Long updateTime ;

        public Node(Node pre,Node next, Object key, Object val) {
            this.pre = pre ;
            this.next = next;
            this.key = key;
            this.val = val;
            this.updateTime = System.currentTimeMillis() ;
        }

        public void setUpdateTime(Long updateTime) {
            this.updateTime = updateTime;
        }

        public Long getUpdateTime() {
            return updateTime;
        }
    }


    /**
     * copy HashMap 的 hash 實現
     * @param key
     * @return
     */
    public int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    /**
     * 判斷是否停止 flag
     */
    private volatile boolean flag = true ;
    private class CheckTimeThread implements Runnable{

        @Override
        public void run() {
            while (flag){
                try {
                    Node node = QUEUE.poll();
                    if (node == null){
                        continue ;
                    }
                    Long updateTime = node.getUpdateTime() ;
                    if ((updateTime - System.currentTimeMillis()) >= EXPIRE_TIME){
                        remove(node.key) ;
                    }
                } catch (Exception e) {                   
                }
            }
        }
    }
}

 


免責聲明!

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



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