LRU算法的Java實現

LRU算法介紹

LRU算法全稱Least Recently Used,也就是檢查最近最少使用的數據的算法。這個算法一般使用在內存淘汰策略中,用於將不經常使用的數據轉移出內存,將空間騰給最近更經常使用的「熱點數據」。java

初識這個算法忘了是在操做系統課仍是計算機組成原理課上,其在Redis、Guava等工具中也有很是普遍的應用,甚至是最核心的思想之一。若是從此須要本身設計系統,即便不本身實現這個算法,LRU的思想也仍然是很重要的。redis

算法很簡單,只須要將全部數據按使用時間排序,在須要篩選出LRU數據時,取排名靠後的便可。算法

算法實現

Redis中的LRU

Redis中的數據量一般很龐大,若是每次對全量數據進行排序,勢必將對服務吞吐量形成影響。所以,Redis在LRU淘汰部分key時,使用的是採樣並計算近似LRU的,所以淘汰的是局部LRU數據。數組

Redis內存淘汰策略dom

maxmemory-policy配置可選參數:ide

  • noeviction:不淘汰,內存超限後寫命令會返回錯誤(如OOM, del命令除外)
  • allkeys-lru:全部key的LRU機制 在全部key中按照最近最少使用LRU原則剔除key,釋放空間
  • volatile-lru:易失key的LRU 僅以設置過時時間key範圍內的LRU(如均爲設置過時時間,則不會淘汰)
  • allkeys-random:全部key隨機淘汰 一視同仁,隨機
  • volatile-random:易失Key的隨機 僅設置過時時間key範圍內的隨機
  • volatile-ttl:易失key的TTL淘汰 按最小TTL的key優先淘汰

Redis LRU的效果工具

左上-理論LRU效果;右上-Redis3.0中的近似LRU(採樣值10);左下-Redis2.8中的近似LRU(採樣值5);右下-Redis3.0中的近似LRU(採樣值5)spa

淺灰色-被淘汰;灰色-未被淘汰;綠色-新寫入操作系統

補充說明:設計

  • 達到設定的內存佔用閾值時纔會進行內存淘汰
  • maxmemory-samples配置表示採樣值,每次刪除時採集的樣本數——採樣值10,表示從設置中定義的key中取10個key進行LRU計算並刪除LRU的那個key
  • Redis3.0中的算法創建了一個「候選池」,使得算法的效率和準確率都比2.8有提升,由於範圍縮小了

結論:

  • Redis3.0經過增長候選池提升了LRU準確性,效果比2.8好
  • 採樣值越高越結果越接近理論LRU(可是採樣值越高效率低)
  • 差很少採樣率5就已經足夠準確了,固然使用10已經基本接近理論LRU結果,可是損失效率

Java中的LRU實現思路

根據LRU算法,在Java中實現須要這些條件:

  • 底層數據使用雙向鏈表,方便在鏈表的任意位置進行刪除,在鏈表尾進行添加
    • 這一點用單鏈表比較費勁,固然用數組等結構也都很費勁
    • 固然雙向鏈表在查找時也麻煩,但下述能夠結合HashMap使用
  • 須要將鏈表按照訪問(使用)順序排序
  • 數據量超過必定閾值後,須要刪除Least Recently Used數據

Java中一個簡單的LRUCache實現

對於上述的實現思路,java.util.LinkedHashMap已經實現了其中的99%,所以直接基於LinkedHashMap實現LRUCache很是簡單。

LinkedHashMap爲LRUCache鋪墊了什麼

  • 構造方法提供了accessOrder選項,開啓後會get方法會有額外操做保證鏈表順序按訪問順序逆序排列
  • 底層結構使用雙向鏈表,查找可使用HashMap的特色
  • 覆蓋了父類HashMap的newNode方法和newTreeNode方法,這兩個方法在HashMap中只是建立Node用的,而在LinkedHashMap中不但建立Node,還將Node放在鏈表末尾
  • 父類HashMap提供了3個void的Hook方法,方法沒作任何事:
    • afterNodeRemoval 父類在remove一個集合中存在的元素後調用
    • afterNodeInsertion 父類在put、compute、merge後調用
    • afterNodeAccess 父類在replace、compute、merge等替換值後會調用,LinkedHashMap在get中開啓accessOrder時調用,究其根本是在對數據有操做時會調用
  • LinkedHashMap本質上仍是複用HashMap的絕大部分功能,包括底層的Node<K, V>[],所以能支持本來HashMap的功能
  • 可是LinkedHashMap實現了父類HashMap的3個Hook方法:
    • afterNodeRemoval 實現鏈表的刪除操做
    • afterNodeInsertion 並無實現鏈表的插入操做,但新添加了一個Hook方法boolean removeEldestEntry,當這個Hook方法返回true時,刪除鏈表頭的節點
    • afterNodeAccess 如前所述,開啓accessOrder後會將被操做的節點放在鏈表末尾,保證鏈表順序按訪問順序逆序排列
  • 上一條3個方法是用來構建雙向鏈表的,LinkedHashMap還覆蓋了父類的3個方法:
    • newNode 在建立一個Node的同時,將Node添加到鏈表末尾
    • newTreeNode 建立TreeNode的同時,將Node添加到鏈表末尾
    • get 完成get功能的同時,若是accessOrder開啓,會調用afterNodeAccess將Node移動到鏈表末尾 覆蓋newNodenewTreeNode方法後,在put方法中調用的newNodenewTreeNode方法也就連帶實現了鏈表的插入操做

綜上,咱們能夠了解到LinkedHashMap爲何可以輕鬆實現LRUCache

  1. 繼承父類HashMap,擁有HashMap的功能,所以在查找一個節點時時間複雜度爲O(1),再加上鍊表是雙向,作鏈表任意節點的刪除工做就很是簡單
  2. 經過HashMap提供的3個Hook方法並覆蓋了2個建立Node的方法,實現了自身鏈表的添加、刪除工做,保證在不影響本來Array功能的前提下,正確完成自身的鏈表構建;這個過程實際上均是經過Hook方式加強原有功能的,由於本來的HashMap中建立節點其實也是使用的Hook方法
  3. 提供屬性accessOrder並實現了afterNodeAccess方法,所以可以根據訪問或操做順序將最近使用或最近插入的數據放在鏈表尾,越久沒被使用的數據就越靠近鏈表頭,實現了整個鏈表按照LRU的要求排序
  4. 提供了一個Hook方法boolean removeEldestEntry,這個方法返回true時將會刪除表頭節點,即LRU中應當淘汰的節點,可是這個方法在LinkedHashMap中的實現永遠返回false

到這爲止,實現一個LRUCache就很簡單了:實現這個removeEldestEntryHook方法,給LinkedHashMap設置一個閾值,那麼超過這個閾值時就會進行LRU淘汰。

網上隨處可見的Java代碼實現

// 繼承LinkedHashMap
	public class LRUCache<K, V> extends LinkedHashMap<K, V> {
		private final int MAX_CACHE_SIZE;

		public LRUCache(int cacheSize) {
			// 使用構造方法 public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
			// initialCapacity、loadFactor都不重要
			// accessOrder要設置爲true,按訪問排序
			super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
			MAX_CACHE_SIZE = cacheSize;
		}

		@Override
		protected boolean removeEldestEntry(Map.Entry eldest) {
			// 超過閾值時返回true,進行LRU淘汰
			return size() > MAX_CACHE_SIZE;
		}

	}
複製代碼

看似幾行代碼解決的事兒,其實只是冰山一角而已。

參考資料

Using Redis as an LRU cache – Redis

本文搬自個人博客,歡迎參觀!

相關文章
相關標籤/搜索