實現一個線程安全並且可以設置過期時間的LRU(LinkedHashMap原理)

目錄

1、HashMap原理

2、LinkedHashMap實現LRU原理(accessOrder = true)

2.1 數據結構

2.2 put方法

2.3 get方法

2.4 remove方法

3、普通LRU代碼實現

4、實現一個線程安全並且可以設置過期時間的LRU緩存

4.1 解決安全問題

4.2 實現定期刪除


FIFO的思想是實現一個先進先出的隊列,LRU最近最久未使用。可以用雙向鏈表linkedList來實現,同時爲了兼顧查詢節點時的效率,結合HashMap來實現。雙向鏈表linkedList+HashMap的數據結構可以聯想到LinkedHashMap,就不需要我們自己來實現了。LinkedHashMap存儲數據是有序的,可以分爲插入順序(accessOrder = false)和訪問順序(accessOrder = true),默認爲插入順序,而且LinkedHashMap提供了刪除最後一個節點的方法removeEldestEntry(Map.Entry eldest),正好可以用來實現FIFO(LinkedHashMap按插入順序存儲數據)和LRU算法(LinkedHashMap按訪問順序存儲數據)。

1、HashMap原理

底層是Entry數組+鏈表(Entry節點的next指針)+紅黑樹。JDK8中,鏈表長度不小於8時,將鏈表轉化爲紅黑樹。默認無參構造函數會初始化大小爲16,向集合中添加元素至集合大小的0.75倍時,會生成一個大小爲原來2倍的新集合,然後重新計算元素的地址,將集合中元素插入到新集合,屆時效率很低。線程不安全。(例如:put的時候導致的數據覆蓋、集合擴展時(resize方法)會出現死循環)。

//HashMap的Entry結構:
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}

2、LinkedHashMap實現LRU原理(accessOrder = true

  • 2.1 數據結構

HashMap的原理是內部維護了一個Entry數組,而LinkedHashMap在HashMap的基礎上,增加了鏈表頭節點和尾節點兩個指針,增加了排序方式的標誌,Entry節點增加了前後兩個指針。因此LinkedHashMap的Entry節點有三個指針,一個是雙向鏈表的前指針、一個是雙向鏈表的後指針、一個是HashMap的hash地址重複時拉鍊法解決衝突的next的指針。

/*LinkedHashMap的Entry結構:*/
//雙向鏈表頭結點
transient LinkedHashMap.Entry<K,V> head;
//雙向鏈表尾節點
transient LinkedHashMap.Entry<K,V> tail;
//是否基於訪問順序排序(默認爲false即插入順序排序)
final boolean accessOrder;
//Entry繼承了HashMap的Entry,又增加了before, after兩個指針
private static class Entry<K,V> extends HashMap.Entry<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
}
  • 2.2 put方法

如果put的是新key,則將Entry節點添加到Map中,並添加到雙向鏈表的尾部,若initialCapacity數量已滿,刪除最近最久未使用的Entry節點即雙向鏈表的頭結點;若put的是已有的key,更新節點的value,並將節點刪除並添加到尾部。

HashMap的put方法會生成一個節點,調用了newNode方法,而LinkedHashMap重寫了此方法
 /**
     * 創建一個節點
     * @param hash  hash值
     * @param key   鍵
     * @param value 值
     * @param e     下一個節點,這個是HashMap節點的屬性
     * @return
     */
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        //調用構造方法
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        //維護鏈表
        linkNodeLast(p);
        return p;
    }

/**
     * 添加一個節點到末尾
     * @param p 節點
     */
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        //保存尾部節點
        LinkedHashMap.Entry<K,V> last = tail;
        //更新尾部節點
        tail = p;
        //判斷之前的尾部節點是否爲空
        if (last == null)
            //之前的尾部節點爲空,說明還沒有數據,設置一下頭節點
            head = p;
        else {
            //說明之前已經有數據了,將新的節點作爲尾部節點連接起來
            p.before = last;
            last.after = p;
        }
    }

HashMap當put一個已經存在的key時,會觸發是否更新的操作,之後會調用afterNodeAccess方法,LinkedHashMap重寫了此方法
/**
     * accessOrder爲true時,將操作的節點移到鏈表尾部
     * @param e 節點
     */
    void afterNodeAccess(Node<K,V> e) {
        LinkedHashMap.Entry<K,V> last;
        //accessOrder 這個參數是指在進行操作的時候,是否將操作的節點移動到鏈表的最後,默認false
        //也就是說accessOrder爲false的時候鏈表就是按照插入順序維護的
        //true的時候,會將最近使用的節點移動到鏈表最後
        if (accessOrder && (last = tail) != e) {
            //保存當前節點和其前置節點和後置節點
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //清空後置節點,因爲當前節點要被移動到最後了
            p.after = null;
            //判斷前置節點是否爲空節點
            if (b == null)
                //前置節點爲空,說明當前節點是頭節點,將它的後置節點也就是第二個節點設置爲頭節點
                head = a;
            else
                //存在前置節點,將前置節點的後置節點連接到當前節點的下一個節點
                b.after = a;
            //判斷後置節點是否爲空
            if (a != null)
                //後置節點不爲空,更新後置節點的前置節點
                a.before = b;
            else
                //說明該節點就是尾部節點,設置前置節點爲後節點
                //a == null 說明p就是尾部節點? 有點不清楚
                last = b;
            //統一更新尾部節點
            if (last == null)
                //說明只有這麼一個節點
                head = p;
            else {
                //將當前節點掛到鏈表末尾
                p.before = last;
                last.after = p;
            }
            //設置尾部節點
            tail = p;
            ++modCount;
        }
    }

LinkedHashMap也重寫了afterNodeInsertion方法
void afterNodeInsertion(boolean evict) {
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }


 需要注意:

removeEldestEntry方法是是否刪除鏈表的頭結點,默認爲不刪除,實現LRU需要覆蓋此方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
  • 2.3 get方法

移動當前操作的節點到鏈表最後

    public V get(Object key) {
        // 調用genEntry得到Entry
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        // 如果LinkedHashMap是訪問順序的,則get時,也需要重新排序
        e.recordAccess(this);
        return e.value;
    }
  • 2.4 remove方法

從Map和鏈表中刪除

LinkedHashMap調用了HashMap的remove方法,重寫了afterNodeRemoval方法

LinkedHashMap調用了HashMap的remove方法
重寫了afterNodeRemoval方法
 /**
     * 刪除鏈表中的節點
     * @param e
     */
    void afterNodeRemoval(Node<K,V> e) {
        //獲取當前節點的前置後置節點
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //清空前置後置節點
        p.before = p.after = null;
        
        if (b == null)
            //前置節點爲空,說明爲頭節點,更新頭節點爲後置節點
            head = a;
        else
            //前置節點不爲空,設置前置節點的後置節點爲刪除節點的後置節點
            b.after = a;
        if (a == null)
            //後置節點爲空,說明爲尾部節點,更新尾部節點爲其前置節點
            tail = b;
        else
            //後置節點不爲空,更新後置節點的前置節點
            a.before = b;
    }

3、普通LRU代碼實現

1、removeEldestEntry方法(是否刪除元素)默認返回false,需要重寫

2、通過LinkedHashMap構造函數中的參數accessOrder來指定數據存儲的順序(false爲插入順序,true爲訪問順序)

//LinkedHashMap構造函數
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
//LRU算法 (FIFO算法只需要將LinkedHashMap的第三個參數true改爲false)
public class LRUCache {
    
    private int capacity;
    
    private LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<Integer, Integer>(capacity,0.75f,true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > capacity;
        }
    };
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
    }
    
    public int get(int key) {
        Integer res = cache.get(key);
        return res;
    }
    
    public void put(int key, int value) {
        cache.put(key, value);
    }
}

4、實現一個線程安全並且可以設置過期時間的LRU緩存

  • 4.1 解決安全問題

線程不安全主要是因爲HashMap和LinkedHashMap都是線程不安全的,而且同時修改map和雙向鏈表之間也會產生併發問題,所以僅僅使用線程安全的ConcurrentHashMap、ConcurrentLinkedQueue並不能解決問題,還要解決map和鏈表間的同步問題,最簡單的方法就是在put或get時直接使用ReenTrantLock進行同步,如com.google.gson包提供的LruCache類,直接在方法上使用synchronized進行同步

package com.google.gson;

import java.util.LinkedHashMap;
import java.util.Map.Entry;

final class LruCache<K, V> extends LinkedHashMap<K, V> implements Cache<K, V> {
    private static final long serialVersionUID = 1L;
    private final int maxCapacity;

    public LruCache(int maxCapacity) {
        super(maxCapacity, 0.7F, true);
        this.maxCapacity = maxCapacity;
    }

    public synchronized void addElement(K key, V value) {
        this.put(key, value);
    }

    public synchronized V getElement(K key) {
        return this.get(key);
    }

    public synchronized V removeElement(K key) {
        return this.remove(key);
    }

    protected boolean removeEldestEntry(Entry<K, V> entry) {
        return this.size() > this.maxCapacity;
    }
}
  • 4.2 實現定期刪除

使用ScheduledThreadPoolExecutor這種定時任務線程池來實現,ScheduledThreadPoolExecutor 使用的任務隊列 DelayQueue 封裝了一個 PriorityQueue,PriorityQueue 會對隊列中的任務進行排序(堆排序),延遲時間最短的放在前面先被執行,如果執行所需時間相同則先提交的任務將被先執行。

具體實現就是增加一個方法,在增加緩存時(put方法中)提交延時任務。具體實現參考(https://zhuanlan.zhihu.com/p/135936339

//過期後清除鍵值對
private void removeAfterExpireTime(K key, long expireTime) {
        scheduledExecutorService.schedule(() -> {
            //1、從map中刪除
            //2、從鏈表刪除
        }, expireTime, TimeUnit.MILLISECONDS);
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章