上一篇文章筆者對Collection包中非常重要的一個類
HashMap
進行了分析和總結, 如果你對HashMap
的知識點有模糊, 建議先讀一讀JDK1.8 Collection知識點與代碼分析–HashMap但是HashMap
存在一些問題, 最重要的是HashMap不能夠保證存儲元素的有序性, 這一點是因爲HashMap的遍歷是按照桶順序的, 而節點的插入順序和桶的順序無關, 並且還會在resize的時候進一步打亂. 那有沒有既能夠在常數時間進行增刪改查又能夠保持有序的Map呢, 於是就有了LinkedHashMap
.
LinkedHashMap
是HashMap
的子類, 所以其落腳點是HashMap
, 在其基礎上, 通過一個雙向鏈表將節點按照插入順序或訪問順序連接起來, 就能夠以較低的成本達到上述的有序的目標.
同時, 因爲其實現了按照訪問順序的排序, 所以也是天然的LRU容器, 官方的文檔中就有建議, 可以用它來作爲LRU緩存, 按照一定的規則刪除最老的節點, 例如當size超過一個值時, 刪除最長時間未訪問的元素. 在文章的最後, 筆者也實現了一個基於LinkedHashMap
的LRU管理的demo. 另一方面, 因爲是HashMap
的子類,LinkedHashMap
也不能保證線程安全, 如果需要進行併發, 需要使用它的synchronized
的裝飾版本. 本文的所有源碼分析都是基於JDK1.8版本.
Constructor
LinkedHashMap
和HashMap
的構造器基本一致, 但是多了accessOrder
參數, 當它爲false
時, 按照鏈表按照插入順序排序, 當它爲true時, 鏈表按照訪問順序排序
// 其他構造器都無法傳入accessOrder參數
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
對元素的put, putIfAbsent, get, getOrDefault, compute , computeIfAbsent, computeIfPresent, merge操作(假設元素存在)都視作對元素的訪問, replace只有元素被替換的時候才視作一次訪問, putAll方法對傳入的每個元素產生一次訪問, 訪問的順序取決於輸入的Map的iterator提供的順序.
Entry
Entry類是HashMap.Node
的子類, 它在Node
的基礎上增加了兩個成員變量, before
和after
. 因此Entry
類中有三個指針: before
和after
分別指向雙向鏈表的前一個節點和後一個節點, 而next
指向桶中的鏈的下一個節點.
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
因此, LinkedHashMap
的結構示意圖大致是這樣,
接下來的問題就是, 添加和刪除節點時link是如何處理的, 以及, 當節點被訪問時, 在鏈表中的順序調整是怎樣實現的, 以及, 如果要用它實現LRU應當怎麼做.
添加和刪除
putVal
LinkedHashMap
沒有重寫put
方法, 而是利用HashMap
中putVal
的良好設計, 對afterNodeAccess
和afterNodeInsertion
兩個回調函數以及newNode
方法進行重寫.
我們首先回顧一下HashMap.putVal
方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 懶惰加載初始化
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 注意這裏不是new Node( )
else {
// 在桶的鏈表或樹中查找key, 如果不存在也是調用newNode方法創建
// 如果超過閾值, 進行樹化
}
if (e != null) { // 如果key存在
...
afterNodeAccess(e);
return oldValue;
}
}
// 遞增modCount, 並判斷是否擴容
afterNodeInsertion(evict);
return null;
}
注意這裏的newNode
函數, 是設計模式中的工廠模式
, 有普通Node和TreeNode兩個版本, 在HashMap
中, 內部就是簡單的調用了new
產生一個新的實例返回, 但是LinkedHashMap
通過重寫這兩個函數, 除了返回一個新對象, 還將這個對象添加到鏈表尾.
此外, LinkedHashMap
重寫了afterNodeInsertion
回調函數, 判斷是否要將鏈表表頭的元素刪除(刪除最老的節點), 重寫了afterNodeAccess
回調函數, 如果是按照訪問順序排序的話, 將被訪問的節點移動到鏈表的末尾.
注意在JDK1.8以前, 這兩個函數分別是Entry.recordAccess
和addEntry
, 1.8把這兩個函數的邏輯部分移到putVal中, 而抽象出兩個回調函數, 供子類重寫, 邏輯上更加的清晰, 也符合面向對象中的開閉原則(開放擴展, 關閉修改).
removeNode
在remove
操作中, 類似的, HashMap
提供了afterNodeRemoval
回調函數, 而LinkedHashMap
重寫了它, 做了雙向鏈表的刪除節點操作.
按照訪問順序排序
之前提到了afterAccess
回調函數中, 實現了將被訪問節點移動到隊尾的操作, 源碼也就是對雙向鏈表的一個操作.
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
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;
}
}
然後在`HashMap中, 在下面這些函數中, 去調用這個回調函數就能夠實現將最近被訪問的節點安排在隊尾.
將最久沒有訪問的節點刪除
上面提到, 每次插入時, 就會調用afterNodeInsertion
這個回調函數, 因此LinkedHashMap
通過重寫這個函數, 來判斷是否將最不長訪問的節點刪除(隊首的節點)
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
注意removeEldestEntry(first)
函數就是判斷第一個節點是否應該被刪除的條件, 這個函數在源碼中默認爲返回false的.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
重寫它, 你就能獲得力量就能夠用來控制LRU策略. 所以最後, 我們一起來寫一個demo用LinkedHashMap
來手寫一個LRU.
LinkedHashMap 手寫LRU
public class MyLRUSample {
public static void main(String[] args) {
LinkedHashMap<Integer, String> LRU
= new LinkedHashMap<Integer, String>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
return size() > 4; // 當緩存超過4個時, 刪除最不長訪問的
}
};
LRU.put(1, "1");LRU.put(2, "2");LRU.put(3, "3");LRU.put(4, "4");
System.out.println("Original order in LRU");
LRU.forEach((k,v)->System.out.println(k + ":" + v));
LRU.get(3);LRU.get(4);LRU.put(2, "Two");LRU.get(3);
System.out.println("After some accesses, the order in LRU");
LRU.forEach((k,v)->System.out.println(k + ":" + v));
LRU.put(5, "5");
System.out.println("1 has been removed.");
LRU.forEach((k,v)->System.out.println(k + ":" + v));
}
}
輸出:
Original order in LRU
1:1
2:2
3:3
4:4
After some accesses, the order in LRU
1:1
4:4
2:Two
3:3
1 has been removed.
4:4
2:Two
3:3
5:5
結果和預期一致, 同時注意到, 重寫過的iterator
的訪問順序是從鏈表頭到鏈表尾的.