fifo頁面置換演算法_「面試」LRU瞭解麼?看看LinkedHashMap如何實現LRU演算法
技術標籤:fifo頁面置換演算法
以下內容均是本人原創,希望你看完之後能有更多更深入的瞭解,歡迎關注➕
問題:使用Java完成一個簡單的LRU演算法
什麼是LRU演算法
LRU(Least Recently Used),也就是最近最少使用。一種有限的空間資源管理的解決方案,會在空間資源不足的情況下移除掉最近沒被使用過的資料,以保證接下來需要的空間資源。
在現在通用的作業系統中為了解決記憶體不足這個問題,提出了虛擬記憶體這種解決方案,其實虛擬記憶體也就是將機器的記憶體分為多個頁面(提個小問題,一個頁面包含了多少kb的空間?),記憶體中只存放當前需要的頁面資訊,暫時不使用的記憶體資料就儲存到磁碟中。這可以很好的解決記憶體不足的問題。當然了這就無故出現頁面交換的情況,使得讀取記憶體的速度降低(磁碟的讀取速度遠小於記憶體的讀取速度),這種方案肯定有利有弊,只需要我們的服務能夠接受這種情況,那就完全沒有問題。
Redis做為一種記憶體資料庫,記憶體大小對資料庫的影響更重要,所以redis也需要及時的移除掉那些過期資料。在redis中有定時清楚、惰性刪除、定期刪除,但是其策略主要分為兩種,基於訪問時間和基於訪問頻率。基於訪問時間就是LRU演算法,看看LRU演算法的圖解過程,如下圖。
- 先定義好一個定長的佇列
- 按照FIFO的流程,依次申請一段空間
- 直到佇列被佔滿了,出現記憶體不足的情況,淘汰策略開始工作
- 會淘汰佇列中最先進入的資料,最先進去的資料也就是最近最久未被使用的資料,然後把其移除出佇列
LRU 演算法小demo
整體的演算法實現沒有太多的難度,維護一個有限長度的佇列的進出,需要移除或者插入資料。時間複雜度可能會是個問題。
- 佇列如果是連結串列,則移除資料的時間複雜度是O(1),但是查詢資料的時間複雜度是O(n)
- 佇列如果是陣列,則移除資料的時間複雜度是O(n),而且移除資料還伴隨著陣列的平行移動,查詢資料也是O(n),除非另外再加一個Map儲存其索引值會使得其查詢的速度降低到O(1),但是卻又提高了空間複雜度
接下來寫個基於陣列的LRU的簡單程式碼
public class LruDemo { private Object[] items; private HashMap map; private int size; private int index; public LruDemo() { this(8); } public LruDemo(int size) { this.size = size; this.items = new Object[size]; this.map = new HashMap<>(16); this.index = 0; } public void put(T t) { Integer value = map.get(t); if (value == null) { if (index >= size) { // 滿了,需要移除第一個元素 for(int i=1; i lruDemo = new LruDemo(6); lruDemo.put("aliace"); lruDemo.put("bob"); lruDemo.put("cat"); lruDemo.put("dog"); lruDemo.put("egg"); lruDemo.getAll(); lruDemo.put("bob"); lruDemo.getAll(); lruDemo.put("fine"); lruDemo.put("good"); lruDemo.getAll(); }}
輸出的結果是
aliacebobcatdogegg======aliacecatdogeggbob======catdogeggbobfinegood======
這只是一種簡單的寫法,而且效率也比較低,現在就來介紹下將要學習的LinkedHashMap
LinkedHashMap
LinkedHashMap是繼承自HashMap的,只是另外添加了排序相關的功能,使得其成為了有序hashmap,關於HashMap的介紹可以看看Java8的HashMap原理學習,接下來重點關注LinkedHashMap相比HashMap拓展了哪些功能呢?
Entry節點資訊
static class Entry extends HashMap.Node { Entry before, after; Entry(int hash, K key, V value, Node next) { super(hash, key, value, next); }}
頭尾節點
transient LinkedHashMap.Entry head;transient LinkedHashMap.Entry tail;
Entry節點就包含了前置節點和後置節點的地址資訊,再加上在LinkedHashMap又中添加了head和tail頭尾節點,這樣就使得之前的連結串列+資料的資料結構基礎上又加上了雙向連結串列,通過雙向連結串列實現有序性,並且 LinkedHashMap = Linked + HashMap。
accessOrder 值
final boolean accessOrder; 是一個非常關鍵的欄位值,暫時按下不表,接下來會知道其真正的含義
get操作
HashMap進行get操作還是很簡單的,通過hash獲取index,再可能涉及到連結串列(紅黑樹)的遍歷操作,在LinkedHashMap中同樣重寫了相關方法
public V get(Object key) { Node e; if ((e = getNode(hash(key), key)) == null) return null; if (accessOrder) afterNodeAccess(e); return e.value;}
進行常規的getNode操作後在找到對應的節點e之後,當accessOrder是true時,呼叫afterNodeAccess方法,從其名稱也可以看出來時訪問節點後的操作。
void afterNodeAccess(Node e) { // move node to last LinkedHashMap.Entry last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry p = (LinkedHashMap.Entry)e, b = p.before, a = p.after; // b 和 a 分別是訪問的節點e的前置和後置節點 p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } // 把其移動到雙向連結串列的尾部節點 tail = p; ++modCount; }}
也就是說當accessOrder為true時,會修改其雙向連結串列的節點順序,而且搜尋整個類也會發現accessOrder只在這裡發揮用處。順帶觀察下其key、value、entry的迭代器遍歷情況,可以發現都是使用了for (LinkedHashMap.Entry e = head; e != null; e = e.after) 這種條件去迴圈遍歷。
所以accessOrder就是起到控制訪問順序的作用,設定為true之後每訪問一個元素,就將該元素移動到雙向連結串列的尾部節點,通過改變節點在雙向連結串列的位置實現對連結串列順序的控制。
put 操作
在HashMap中通過put方法插入一個新的節點資料,LinkedHashMap並沒有重寫該方法。在HashMap中先檢查是否存在對應的key,如果不存在則會通過newNode方法建立一個新節點,然後等待插入到合適的位置,LinkedHashMap則重寫了newNode方法,如下程式碼塊:
Node newNode(int hash, K key, V value, Node e) { LinkedHashMap.Entry p = new LinkedHashMap.Entry(hash, key, value, e); linkNodeLast(p); return p;}// link at the end of listprivate void linkNodeLast(LinkedHashMap.Entry p) { LinkedHashMap.Entry last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; }}
建立完一個LinkedHashMap.Entry節點p後,p節點的before, after都是null,然後呼叫linkNodeLast方法,採取尾插法,形成新的尾節點(這裡有一種情況是最早的時候tail==head==null的情況,會使得頭節點和尾節點都指向同一個節點)。
新插入一個節點後還會呼叫afterNodeInsertion方法,看起方法名稱也知道是在node節點插入後的操作,在HashMap中是空實現,在LinkedHashMap則實現了該方法,如下程式碼塊:
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); }} protected boolean removeEldestEntry(Map.Entry eldest) { return false;}
預設傳入的evict值是true,而removeEldestEntry方法預設返回false,也就是什麼都不做。當在put一個已經存在的節點的情況,會呼叫afterNodeAccess方法,也會去改變在連結串列中的位置。重寫removeEldestEntry方法並且當起返回true時,呼叫removeNode節點移除head節點。這個就包含了LRU最近最少使用的實現原理。
自定義LRU演算法
設定accessOrder為true後,每次訪問、新增的節點都會移動到尾部,當removeEldestEntry返回true時就會移除頭節點,那麼只需要設定一種特定的判斷邏輯使得removeEldestEntry返回true就可以了。按照上面LRU演算法的思想,只有當空間滿了的情況下才會移除頭節點資料,同理只需要判斷當前map中的節點數是否達到相關的閾值即可。繼承LinkedHashMap過載removeEldestEntry方法,程式碼如下:
public class LruMap extends LinkedHashMap { private int maxSize; public LruMap(int initialCapacity, float loadFactor, boolean accessOrder, int maxSize) { super(initialCapacity, loadFactor, accessOrder); this.maxSize = maxSize; } public LruMap(int maxSize) { this(16, 0.75f, true, maxSize); } public LruMap(int tableSize, int maxSize) { this(tableSize, 0.75f, true, maxSize); } @Override protected boolean removeEldestEntry(Map.Entry eldest) { boolean flag = size() > maxSize; if (flag) { System.out.println("移除頭節點, key:" + eldest.getKey() +