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
擴展:
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) { } } } } }