探索WeakHashMap底層實現
前言
探索WeakHashMap
底層實現是基於JDK1.8
,它的資料結構是陣列 + 連結串列
。就不貼它的註釋了,直接總結一下吧:
WeakHashMap基於
弱鍵
實現了Map介面,也就是說,當某個鍵不在使用時會被丟棄,對應的鍵值對將會被自動移除。如何確定不在使用取決於GC是否執行,而對於GC何時執行我們並不知道,所以某個鍵何時被丟棄我們也不得而知,至於GC如何執行就是另外一個話題了,有可能導致上一分鐘與下一分鐘獲取到的結果是不一致的。另一個方面,WeakHashMap的值物件由強引用所持有(何為強引用下面會介紹),應確保值物件不會直接或間接引用自身的鍵或其他鍵,這會導致鍵無法被丟棄。
-
強引用:簡單來說指向new出來的物件就是一個強引用,可以說是經常使用。對於強引用來說,它們不會被GC回收,即使記憶體空間不足,JVM寧願丟擲記憶體溢位錯誤也不敢動它們,總體來說還是很有威信的。
-
軟引用:首先給強引用包裹上一層
SoftReference
,通過SoftReference獲取到的引用即為軟引用。對於軟引用來說,在記憶體充足的情況下,GC可以選擇性的清除,而一旦記憶體不足了,它們一個都跑不了,都會被清除掉。軟引用最常用用於實現對記憶體敏感的快取。 -
弱引用:首先給強引用包裹上一層
WeakReference
,通過WeakReference獲取到的引用即為弱引用,看到這裡你應該就已經明白了WeakHashMap內部的機制。對於弱引用來說,GC壓根就不管記憶體是否充足,直接回收,很沒有人性! -
虛引用:首先給強引用包裹上一層
PhantomReference
,通過PhantomReference獲取到的引用即為虛引用。對於虛引用來說,它在任何時候都可能被回收,常用於跟蹤物件。
還有一個方面,讀者最好去了解下Reference
類,內部通過佇列實現了一些機制。
資料結構
前奏都準備好了,開始進入正題吧。
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> { /** * 預設初始容量,必須是2的冪次方,可參考HashMap */ private static final int DEFAULT_INITIAL_CAPACITY = 16; /** * 最大容量,必須是2的冪次方 */ private static final int MAXIMUM_CAPACITY = 1 << 30; /** * 預設載入因子 */ private static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 雜湊表,長度必須是2的冪次方 */ Entry<K,V>[] table; /** * 雜湊表中包含節點的個數 */ private int size; /** * 擴容前需要判斷的閾值 * 若超過該值則擴容,若沒超過則不需要 * 該值的計算方式:capacity * load factor */ private int threshold; /** * 載入因子 */ private final float loadFactor; /** * 引用佇列 * * 為什麼需要引用佇列呢? * 通過上面的介紹我們可以知道雜湊表中某些鍵可能會被移除掉,而移除是GC幫我們做的,那WeakHashMap怎麼知道哪些鍵被移除掉了以便更新自己的鍵值對,就是該佇列做了它們兩個之間的媒介 * 上面讓讀者去了解Reference類,下面講的內容其實都在該類中有提到,比較簡單 * GC在丟棄某個鍵時會將它的鍵值對,也就是節點資訊存放到Reference類中的pending佇列中,Reference類在初始化時會啟動一個執行緒,那麼該執行緒會將pending佇列中的節點資訊放入到queue佇列中 * 也就是在告訴WeakHashMap,佇列中的這些節點是我要刪除的,你記得更新 */ private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); /** * 快取entrySet方法的返回值 */ private transient Set<Map.Entry<K,V>> entrySet; /** * 結構修改的次數 */ int modCount; }
建構函式
/**
* 指定初始容量與載入因子構造雜湊表
* 在上面中我們提到了容量必須是2的冪次方,所以呼叫tableSizeFor方法來進行調整
* Float.isNaN:檢測是否是數字
* @param initialCapacity 指定初始容量
* @param loadFactor 指定載入因子
*/
public WeakHashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Initial Capacity: "+ initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load factor: "+ loadFactor);
int capacity = 1;
while (capacity < initialCapacity) //這段程式碼有點精髓啊,個人感覺比HashMap中的演算法簡單,兩者要表達的意思是一致的,都是獲取大於initialCapacity的最小值
capacity <<= 1;
table = newTable(capacity);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
}
/**
* 指定初始容量與預設載入因子(0.75)構造雜湊表
* @param initialCapacity 指定初始容量
*/
public WeakHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 預設初始容量(16)與預設載入因子(0.75)構造雜湊表
*/
public WeakHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* 將指定集合新增到雜湊表中,採用預設載入因子
* @param m 指定集合
*/
public WeakHashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);//Math.max是為了獲取儘可能大的容量
putAll(m);
}
簡單方法
/**
* 根據指定長度構造雜湊表
* @param n 指定長度
* @return 雜湊表
*/
@SuppressWarnings("unchecked")
private Entry<K,V>[] newTable(int n) {
return (Entry<K,V>[]) new Entry<?,?>[n];
}
/**
* 倘若鍵為null則採用NULL_KEY作為鍵
* 正如方法名一樣,隱藏Null
* @param key 指定鍵
* @return NULL_KEY或指定鍵
*/
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
/**
* 倘若鍵為NULL_KEY則返回null
* 正如方法名一樣,揭露Null
* @param key 雜湊表中的鍵
* @return null或指定鍵
*/
static Object unmaskNull(Object key) {
return (key == NULL_KEY) ? null : key;
}
/**
* 兩個物件是否相等
* @param x 物件
* @param y 另外一個物件
* @return 是否相等
*/
private static boolean eq(Object x, Object y) {
return x == y || x.equals(y);
}
/**
* 計算雜湊值
* 這邊的計算雜湊值比HashMap複雜多了,涉及到演算法的內容我感覺我沒辦法理解到位
* @param k 物件
* @return 雜湊值
*/
final int hash(Object k) {
int h = k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 計算雜湊表中的索引
* @param h 雜湊值
* @param length 雜湊表的長度
* @return 索引
*/
private static int indexFor(int h, int length) {
return h & (length-1);
}
/**
* 清除雜湊表中過時的節點資訊
* 過時指的是已經被丟棄的鍵,也可以說是被GC回收的鍵
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {//poll:佇列中獲取首部節點並刪除
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i]; //代表當前節點的上一個節點
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e) //說明當前節點是連結串列的首部節點
table[i] = next;
else //說明當前節點不是首部節點
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
/**
* 獲取雜湊表
* @return 雜湊表
*/
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}
/**
* 獲取雜湊表的長度
* @return 雜湊表的長度
*/
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();
return size;
}
/**
* 雜湊表是否為空
* @return 雜湊表是否為空
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 指定鍵獲取指
* @param key 指定鍵
* @return null或值
*/
public V get(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int index = indexFor(h, tab.length);
Entry<K,V> e = tab[index];
while (e != null) {
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}
/**
* 是否包含指定鍵
* @param key 指定鍵
* @return 是否包含指定鍵
*/
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
/**
* 指定鍵獲取節點
* @param key 指定鍵
* @return null或節點
*/
Entry<K,V> getEntry(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int index = indexFor(h, tab.length);
Entry<K,V> e = tab[index];
while (e != null && !(e.hash == h && eq(k, e.get())))
e = e.next;
return e;
}
/**
* 新增節點
* 連結串列中採用頭插法的方式進行新增節點
* 若超過閾值則會進行擴容
* @param key 指定鍵
* @param value 指定值
* @return null或舊值
*/
public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length); //獲取索引
for (Entry<K,V> e = tab[i]; e != null; e = e.next) { //連結串列中判斷是否重複
if (h == e.hash && eq(k, e.get())) {
V oldValue = e.value;
if (value != oldValue)
e.value = value;
return oldValue;
}
}
modCount++;
Entry<K,V> e = tab[i];
tab[i] = new Entry<>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
void resize(int newCapacity) {
Entry<K,V>[] oldTable = getTable();
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry<K,V>[] newTable = newTable(newCapacity);
transfer(oldTable, newTable); //將源雜湊表中的所有節點資訊複製到目標雜湊表中
table = newTable;
/**
* 如果忽略null元素並處理佇列導致大量收縮,則還原舊錶。 這應該很少見,但是可以避免持有大量無用節點的雜湊表的無限擴充套件。
*/
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else { //GC回收了大量的節點後則不進行擴容
expungeStaleEntries(); //檢測新表中哪些節點已經被丟棄了
transfer(newTable, oldTable);
table = oldTable;
}
}
/**
* 將源雜湊表中的所有節點資訊複製到目標雜湊表中
* 源雜湊表中可能出現被丟棄的鍵
* @param src 源雜湊表
* @param dest 目標雜湊表
*/
private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
for (int j = 0; j < src.length; ++j) {
Entry<K,V> e = src[j];
src[j] = null;
while (e != null) {
Entry<K,V> next = e.next;
Object key = e.get(); //若當前節點已經被GC回收了,則此方法返回將返回null
if (key == null) {
e.next = null; // Help GC
e.value = null; // " "
size--;
} else {
int i = indexFor(e.hash, dest.length); //該索引出現的可能應該跟HashMap是一樣的,原索引或與原索引 + 舊容量的大小,只不過它是一個一個的計算並新增,而HashMap是分批計算,一次性新增
e.next = dest[i];
dest[i] = e;
}
e = next;
}
}
}
/**
* 批量新增節點到雜湊表中
* @param m 集合
*/
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
/**
* 倘若指定集合的鍵值對數量超過閾值則進行擴容. 這是保守的;
* 很明顯的條件應該是 (m.size + size) >= threshold, 但是這個條件會導致適當的容量變成2倍,如果被新增的鍵已經存在於雜湊表中.
* 通過使用保守的計算,我們最多隻能調整一種尺寸。
*/
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length) //預先計算好要新增節點的數量以便進行一次性擴容
resize(newCapacity);
}
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
/**
* 指定鍵移除節點
* @param key 指定鍵
* @return null或值
*/
public V remove(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
Entry<K,V> prev = tab[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
if (h == e.hash && eq(k, e.get())) {
modCount++;
size--;
if (prev == e)
tab[i] = next;
else
prev.next = next;
return e.value;
}
prev = e;
e = next;
}
return null;
}
/**
* 指定鍵移除節點是否成功
* @param o 指定鍵
* @return 移除節點是否成功
*/
boolean removeMapping(Object o) {
if (!(o instanceof Map.Entry))
return false;
Entry<K,V>[] tab = getTable();
Map.Entry<?,?> entry = (Map.Entry<?,?>)o;
Object k = maskNull(entry.getKey());
int h = hash(k);
int i = indexFor(h, tab.length);
Entry<K,V> prev = tab[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
if (h == e.hash && e.equals(entry)) {
modCount++;
size--;
if (prev == e)
tab[i] = next;
else
prev.next = next;
return true;
}
prev = e;
e = next;
}
return false;
}
/**
* 清空雜湊表
*/
public void clear() {
while (queue.poll() != null) //清空佇列中只有一部分過時節點
;
modCount++;
Arrays.fill(table, null); //清空雜湊表後
size = 0;
/**
* 清空雜湊表後可能導致GC,另外一部分節點會被新增到佇列中,所以此處需要再次清空佇列
*/
while (queue.poll() != null)
;
}
/**
* 雜湊表中是否包含指定值
* @param value 指定值
* @return 是否包含指定值
*/
public boolean containsValue(Object value) {
if (value==null)
return containsNullValue();
Entry<K,V>[] tab = getTable();
for (int i = tab.length; i-- > 0;)
for (Entry<K,V> e = tab[i]; e != null; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
/**
* 雜湊表中是否包含null值
* @return 是否包含null值
*/
private boolean containsNullValue() {
Entry<K,V>[] tab = getTable();
for (int i = tab.length; i-- > 0;)
for (Entry<K,V> e = tab[i]; e != null; e = e.next)
if (e.value==null)
return true;
return false;
}
/**
* 雜湊表中的節點,該類繼承了WeakReference加上呼叫了父類的構造,說明它的鍵是個弱引用
* 該類中的其他方法就不做展示了,比較簡單
*/
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* 初始化
* 指定鍵生成弱引用
* @param key 指定鍵
* @param value 指定值
* @param queue 與弱引用關聯的佇列
* @param hash 雜湊值
* @param next 下一個節點
*/
Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
}
/**
* 遍歷所有鍵並執行指定動作
* 遍歷過程中不允許WeakHashMap呼叫任何會修改結構的方法,否則最後會丟擲異常
* @param action 指定動作
*/
public void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
int expectedModCount = modCount;
Entry<K, V>[] tab = getTable();
for (Entry<K, V> entry : tab) {
while (entry != null) {
Object key = entry.get();
if (key != null) {
action.accept((K)WeakHashMap.unmaskNull(key), entry.value);
}
entry = entry.next;
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
}
}
}
/**
* 遍歷雜湊表並執行指定動作後獲取新值,利用新值替換所有節點的舊值
* @param function 指定動作
*/
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
int expectedModCount = modCount;
Entry<K, V>[] tab = getTable();;
for (Entry<K, V> entry : tab) {
while (entry != null) {
Object key = entry.get();
if (key != null) {
entry.value = function.apply((K)WeakHashMap.unmaskNull(key), entry.value);
}
entry = entry.next;
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
}
}
}
//一些重複性的東西,比如包含鍵、值、鍵值對的迭代器、可分割迭代器就不講解了,可參考HashMap
總結
-
WeakHashMap的鍵值對允許為null。
-
WeakHashMap採用弱鍵,當某個鍵不在使用時會被GC回收,而鍵對應的節點也會被移除掉。
-
WeakHashMap無序不可重複、非執行緒安全。
-
在新增節點,值物件最好不要與任何的鍵直接或間接的關聯,否則GC無法丟棄該鍵。
-
WeakHashMap#ReferendeQueue是用來檢視雜湊表中哪些鍵被丟棄了,以便雜湊表能夠及時更新。
-
WeakHashMap的容量必須是2的冪次方。
-
WeakHashMap在新增節點時採用的是頭插法。
重點關注
弱鍵
ReferenceQueue
頭插法
強、軟、弱、虛引用
Reference