LinkedHashMap(實現LRU緩存)

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

底層經過LinkedList+HashMap實現node

關鍵屬性:

/** * 雙向鏈表頭節點 */ 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/67024app

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

擴展:

1.LRU-Kui

              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要多。this

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隊列淘汰末尾的數據。spa

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) { } } } } }
相關文章
相關標籤/搜索