LRU,全稱Least Recently Used,即最近最久未使用算法,用於操做系統的頁面置換算法,以及一些常見的框架。其原理實質就是當須要淘汰數據時,會選擇那些最近沒有使用過的數據進行淘汰,換句話說,當某數據被訪問時,就把其移動到淘汰隊列的隊首(也就是最不會被淘汰的位置)java
基於這樣的原則,咱們就能夠着手實現了。不過Java已經爲咱們提供了一個現成的模板,咱們站在巨人的肩膀上,能夠參考一下Java是如何實現LRU功能的算法
在LinkedHashMap中,有一個不多用到的構造函數:緩存
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
複製代碼
其中accessOrder
這一屬性,在其餘的構造函數中是默認爲false
的,若是咱們經過該構造函數將其設爲true
以後,就實現了LRU功能,下面的程序簡單了作了下演示:app
public static void main(String[] args) {
int cacheSize = 3;
// 最大容量 = (緩存大小 / 負載因子)+ 1,保證不會觸發自動擴容
LinkedHashMap<String, String> cache = new LinkedHashMap<String, String>(
(int)(cacheSize/ 0.75f) + 1, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > cacheSize;
}
};
cache.put("1", "a");
cache.put("2", "b");
cache.put("3", "c");
// head => "1" => "2" => "3" => null
// put已存在的值,和get方法是同樣的效果
cache.put("1", "a");
// head => "2" => "3" => "1" => null;
cache.put("4", "d");
// head => "3" => "1" => "4" => null;
for (String key: cache.keySet()) {
System.out.println(key);
}
}
複製代碼
其實還有很重要的一點,就是須要重寫removeEldestEntry()
這一方法,默認是返回false
的,當返回true
時,會移除最久沒有使用的節點,因此咱們要作的,就是當容量達到緩存限制時,移除LRU算法斷定的最近最久未使用節點框架
能夠看到,咱們依次插入節點一、二、3後,若是此時再插入節點4,就會致使removeEldestEntry()
返回爲true
,而後移除隊首節點,即節點1。可是咱們這裏因爲中間重複插入了一次節點1,因此會判斷節點1是「常常訪問的節點」,因此節點1被提到鏈表最後,隊首節點就變成了節點2,當容量超過限制時,會把節點2移除ide
探索LinkedHashMap中LRU的實現原理,咱們就要追溯到HashMap中的putVal
方法,這個方法最後觸發了一個回調函數:函數
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// ...
if (e != null) { // existing mapping for key
// ...
afterNodeAccess(e);
return oldValue;
}
}
afterNodeInsertion(evict);
return null;
}
複製代碼
putVal()
方法在插入後會觸發方法的回調,有兩種狀況:this
afterNodeAccess(e)
afterNodeInsertion(evict)
其中,變量e是「撞車」的節點,變量evict在子類不重寫put()
方法的狀況下是默認爲true
的,因此咱們就把它看成常量來看spa
而後咱們回到,LinkedHashMap中,來看這個兩個鉤子方法(HashMap中這兩個方法實現均爲空):操作系統
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
// 如下狀況知足時,調用removeNode移除最久未使用的節點:
// 1. evict爲true
// 2. 頭結點不爲空
// 3. 符合移除條件:removeEldestEntry返回true
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// 開啓LRU模式,且訪問的節點不是尾節點,則將被訪問的節點置於鏈表尾
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
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
複製代碼
顯而易見,afterNodeInsertion
負責在插入以後判斷是否須要移除最近最久未使用的節點(即鏈表頭節點),afterNodeAccess
負責在訪問某節點以後,將該節點移動到鏈表尾
在afterNodeAccess
中,由於要考慮到各類特殊狀況,並且是一個帶有頭尾節點的雙向鏈表,因此狀況判斷比較複雜,實際上就是將指定節點移動到隊尾,若是本身想實現一個相似的功能能夠不作的這麼複雜
通常來講,若是想作一個LRU算法實現的話,LinkedHashMap就能知足須要了。要是想本身實現的話,這裏提供一個實現的思路: