WeakHashMap 和 HashMap 區別及使用場景
引言
在之前的文章裡,我們聊到瞭 Java 標準庫中 HashMap 與 LinkedHashMap 的實現原理。HashMap 是一個標準的散列表數據結構,而 LinkedHashMap 是在 HashMap 的基礎上實現的哈希鏈表。
今天,我們來討論 WeakHashMap,其中的 “Weak” 是指什麼,與前兩者的使用場景有何不同?我們就圍繞這些問題展開。
學習路線圖:
1. 回顧 HashMap 和 LinkedHashMap
其實,WeakHashMap 與 HashMap 和 LinkedHashMap 的數據結構大同小異,所以我們先回顧後者的實現原理。
1.1 說一下 HashMap 的實現結構
HashMap 是基於分離鏈表法解決散列沖突的動態散列表。
- 1、HashMap 在 Java 7 中使用的是 “數組 + 鏈表”,發生散列沖突的鍵值對會用頭插法添加到單鏈表中;
- 2、HashMap 在 Java 8 中使用的是 “數組 + 鏈表 + 紅黑樹”,發生散列沖突的鍵值對會用尾插法添加到單鏈表中。如果鏈表的長度大於 8 時且散列表容量大於 64,會將鏈表樹化為紅黑樹。
散列表示意圖
HashMap 實現示意圖
1.2 說一下 LinkedHashMap 的實現結構
LinkedHashMap 是繼承於 HashMap 實現的哈希鏈表。
- 1、LinkedHashMap 同時具備雙向鏈表和散列表的特點。當 LinkedHashMap 作為散列表時,主要體現出 O(1) 時間復雜度的查詢效率。當 LinkedHashMap 作為雙向鏈表時,主要體現出有序的特性;
- 2、LinkedHashMap 支持 FIFO 和 LRU 兩種排序模式,默認是 FIFO 排序模式,即按照插入順序排序。Android 中的 LruCache 內存緩存和 DiskLruCache 磁盤緩存也是直接復用 LinkedHashMap 提供的緩存管理能力。
LinkedHashMap 示意圖
FIFO 與 LRU 策略
2. 認識 WeakHashMap
2.1 WeakReference 弱引用的特點
WeakHashMap 中的 “Weak” 指鍵 Key 是弱引用,也叫弱鍵。弱引用是 Java 四大引用類型之一,一共有四種引用類型,分別是強引用、軟引用、弱引用和虛引用。我將它們的區別概括為 3 個維度:
維度 1 – 對象可達性狀態的區別: 強引用指向的對象是強可達的,隻有強可達的對象才會認為是存活的對象,才能保證在垃圾收集的過程中不會被回收;
維度 2 – 垃圾回收策略的區別: 不同的引用類型的回收激進程度不同,
- 強引用指向的對象不會被回收;
- 軟引用指向的對象在內存充足時不會被回收,在內存不足時會被回收;
- 弱引用和虛引用指向的對象無論在內存是否充足的時候都會被回收;
維度 3 – 感知垃圾回收時機: 當引用對象關聯的實際對象被垃圾回收時,引用對象會進入關聯的引用隊列,程序可以通過觀察引用隊列的方式,感知對象被垃圾回收的時機。
感知垃圾回收示意圖
提示: 關於 “Java 四種引用類型” 的區別,在小彭的 Java 專欄中深入討論過 《吊打面試官:說一下 Java 的四種引用類型》,去看看。
2.2 WeakHashMap 的特點
WeakHashMap 是使用弱鍵的動態散列表,用於實現 “自動清理” 的內存緩存。
- 1、WeakHashMap 使用與 Java 7 HashMap 相同的 “數組 + 鏈表” 解決散列沖突,發生散列沖突的鍵值對會用頭插法添加到單鏈表中;
- 2、WeakHashMap 依賴於 Java 垃圾收集器自動清理不可達對象的特性。當 Key 對象不再被持有強引用時,垃圾收集器會按照弱引用策略自動回收 Key 對象,並在下次訪問 WeakHashMap 時清理全部無效的鍵值對。因此,WeakHashMap 特別適合實現 “自動清理” 的內存活動緩存,當鍵值對有效時保留,在鍵值對無效時自動被垃圾收集器清理;
- 3、需要註意,因為 WeakHashMap 會持有 Value 對象的強引用,所以在 Value 對象中一定不能持有 key 的強引用。否則,會阻止垃圾收集器回收 “本該不可達” 的 Key 對象,使得 WeakHashMap 失去作用。
- 4、與 HashMap 相同,LinkedHashMap 也不考慮線程同步,也會存在線程安全問題。可以使用 Collections.synchronizedMap 包裝類,其原理也是在所有方法上增加 synchronized 關鍵字。
WeakHashMap 示意圖
2.3 說一下 WeakHashMap 與 HashMap 和 LinkedHashMap 的區別?
WeakHashMap 與 HashMap 都是基於分離鏈表法解決散列沖突的動態散列表,兩者的主要區別在鍵 Key 的引用類型上:
HashMap 會持有鍵 Key 的強引用,除非手動移除,否則鍵值對會長期存在於散列表中。而 WeakHashMap 隻持有鍵 Key 的弱引用,當 Key 對象不再被外部持有強引用時,鍵值對會被自動被清理。
WeakHashMap 與 LinkedHashMap 都有自動清理的能力,兩者的主要區別在於淘汰數據的策略上:
LinkedHashMap 會按照 FIFO 或 LRU 的策略 “嘗試” 淘汰數據,需要開發者重寫 removeEldestEntry()
方法實現是否刪除最早節點的判斷邏輯。而 WeakHashMap 會按照 Key 對象的可達性淘汰數據,當 Key 對象不再被持有強引用時,會自動清理無效數據。
2.4 重建 Key 對象不等價的問題
WeakHashMap 的 Key 使用弱引用,也就是以 Key 作為清理數據的判斷錨點,當 Key 變得不可達時會自動清理數據。此時,如果使用多個 equals
相等的 Key 對象訪問鍵值對,就會出現第 1 個 Key 對象不可達導致鍵值對被回收,而第 2 個 Key 查詢鍵值對為 null 的問題。這說明 equals
相等的 Key 對象在 HashMap 等散列表中是等價的,但是在 WeakHashMap 散列表中是不等價的。
因此,如果 Key 類型沒有重寫 equals 方法,那麼 WeakHashMap 就表現良好,否則會存在歧義。例如下面這個 Demo 中,首先創建瞭指向 image_url1
的圖片 Key1,再重建瞭同樣指向 image_url1
的圖片 Key2。在 HashMap 中,Key1 和 Key2 等價,但在 WeakHashMap 中,Key1 和 Key2 不等價。
Demo
class ImageKey { private String url; ImageKey(String url) { this.url = url; } public boolean equals(Object obj) { return (obj instanceOf ImageKey) && Objects.equals(((ImageKey)obj).url, this.url); } } WeakHashMap<ImageKey, Bitmap> map = new WeakHashMap<>(); ImageKey key1 = new ImageKey("image_url1"); ImageKey key2 = new ImageKey("image_url2"); // key1 equalsTo key3 ImageKey key3 = new ImageKey("image_url1"); map.put(key1, bitmap1); map.put(key2, bitmap2); System.out.println(map.get(key1)); // 輸出 bitmap1 System.out.println(map.get(key2)); // 輸出 bitmap2 System.out.println(map.get(key3)); // 輸出 bitmap1 // 使 key1 不可達,key3 保持 key1 = null; // 說明重建 Key 與原始 Key 不等價 System.out.println(map.get(key1)); // 輸出 null System.out.println(map.get(key2)); // 輸出 bitmap2 System.out.println(map.get(key3)); // 輸出 null
默認的 Object#equals
是判斷兩個變量是否指向同一個對象:
Object.java
public boolean equals(Object obj) { return (this == obj); }
2.5 Key 弱引用和 Value 弱引用的區別
不管是 Key 還是 Value 使用弱引用都可以實現自動清理,至於使用哪一種方法各有優缺點,適用場景也不同。
Key 弱引用: 以 Key 作為清理數據的判斷錨點,當 Key 不可達時清理數據。優點是容器外不需要持有 Value 的強引用,缺點是重建的 Key 與原始 Key 不等價,重建 Key 無法阻止數據被清理;
Value 弱引用: 以 Value 作為清理數據的判斷錨點,當 Value 不可達時清理數據。優點是重建 Key 與與原始 Key 等價,缺點是容器外需要持有 Value 的強引用。
類型 | 優點 | 缺點 | 場景 |
---|---|---|---|
Key 弱引用 | 外部不需要持有 Value 的強引用,使用更簡單 | 重建 Key 不等價 | 未重寫 equals |
Value 弱引用 | 重建 Key 等價 | 外部需要持有 Value 的強引用 | 重寫 equals |
舉例 1: 在 Android Glide 圖片框架的多級緩存中,因為圖片的 EngineKey 是可重建的,存在多個 EngineKey 對象指向同一個圖片 Bitmap,所以 Glide 最頂層的活動緩存采用的是 Value 弱引用。
EngineKey.java
class EngineKey implements Key { // 重寫 equals @Override public boolean equals(Object o) { if (o instanceof EngineKey) { EngineKey other = (EngineKey) o; return model.equals(other.model) && signature.equals(other.signature) && height == other.height && width == other.width && transformations.equals(other.transformations) && resourceClass.equals(other.resourceClass) && transcodeClass.equals(other.transcodeClass) && options.equals(other.options); } return false; } }
舉例 2: 在 ThreadLocal 的 ThreadLocalMap 線程本地存儲中,因為 ThreadLocal 沒有重寫 equals,不存在多個 ThreadLocal 對象指向同一個鍵值對的情況,所以 ThreadLocal 采用的是 Key 弱引用。
ThreadLocal.java
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
3. WeakHashMap 源碼分析
這一節,我們來分析 WeakHashMap 中主要流程的源碼。
事實上,WeakHashMap 就是照著 Java 7 版本的 HashMap 依葫蘆畫瓢的,沒有樹化的邏輯。考慮到我們已經對 HashMap 做過詳細分析,所以我們沒有必要重復分析 WeakHashMap 的每個細節,而是把重心放在 WeakHashMap 與 HashMap 不同的地方。
3.1 WeakHashMap 的屬性
先用一個表格整理 WeakHashMap 的屬性:
版本 | 數據結構 | 節點實現類 | 屬性 |
---|---|---|---|
Java 7 HashMap | 數組 + 鏈表 | Entry(單鏈表) | 1、table(數組) 2、size(尺寸) 3、threshold(擴容閾值) 4、loadFactor(裝載因子上限) 5、modCount(修改計數) 6、默認數組容量 16 7、最大數組容量 2^30 8、默認負載因子 0.75 |
WeakHashMap | 數組 + 鏈表 | Entry(單鏈表,弱引用的子類型) | 9、queue(引用隊列) |
WeakHashMap.java
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> { // 默認數組容量 private static final int DEFAULT_INITIAL_CAPACITY = 16; // 數組最大容量:2^30(高位 0100,低位都是 0) private static final int MAXIMUM_CAPACITY = 1 << 30; // 默認裝載因子上限:0.75 private static final float DEFAULT_LOAD_FACTOR = 0.75f; // 底層數組 Entry<K,V>[] table; // 鍵值對數量 private int size; // 擴容閾值(容量 * 裝載因子) private int threshold; // 裝載因子上限 private final float loadFactor; // 引用隊列 private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 修改計數 int modCount; // 鏈表節點(一個 Entry 等於一個鍵值對) private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { // 與 HashMap 和 LinkedHashMap 相比,少瞭 key 的強引用 // final K key; // Value(強引用) V value; // 哈希值 final int hash; Entry<K,V> next; Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) { super(key /*註意:隻有 Key 是弱引用*/, queue); this.value = value; this.hash = hash; this.next = next; } } }
WeakHashMap 與 HashMap 的屬性幾乎相同,主要區別有 2 個:
- 1、ReferenceQueue: WeakHashMap 的屬性裡多瞭一個 queue 引用隊列;
- 2、Entry:
WeakHashMap#Entry
節點繼承於WeakReference
,表面看是 WeakHashMap 持有瞭 Entry 的強引用,其實不是。註意看 Entry 的構造方法,WeakReference 關聯的實際對象是 Key。 所以,WeakHashMap 依然持有 Entry 和 Value 的強引用,僅持有 Key 的弱引用。
引用關系示意圖
不出意外的話又有小朋友出來舉手提問瞭🙋🏻♀️:
- 🙋🏻♀️疑問 1:說一下 ReferenceQueue queue 的作用?
ReferenceQueue 與 Reference 配合能夠實現感知對象被垃圾回收的能力。在創建引用對象時可以關聯一個實際對象和一個引用隊列,當實現對象被垃圾回收後,引用對象會被添加到這個引用隊列中。在 WeakHashMap 中,就是根據這個引用隊列來自動清理無效鍵值對。
- 🙋🏻♀️疑問 2:為什麼 Key 是弱引用,而不是 Entry 或 Value 是弱引用?
首先,Entry 一定要持有強引用,而不能持有弱引用。這是因為 Entry 是 WeakHashMap 內部維護數據結構的實現細節,並不會暴露到 WeakHashMap 外部,即除瞭 WeakHashMap 本身之外沒有其它地方持有 Entry 的強引用。所以,如果持有 Entry 的弱引用,即使 WeakHashMap 外部依然在使用 Key 對象,WeakHashMap 內部依然會回收鍵值對,這與預期不符。
其次,不管是 Key 還是 Value 使用弱引用都可以實現自動清理。至於使用哪一種方法各有優缺點,適用場景也不同,這個在前文分析過瞭。
3.2 WeakHashMap 如何清理無效數據?
在通過 put / get /size 等方法訪問 WeakHashMap 時,其內部會調用 expungeStaleEntries()
方法清理 Key 對象已經被回收的無效鍵值對。其中會遍歷 ReferenceQueu 中持有的弱引用對象(即 Entry 節點),並將該結點從散列表中移除。
private final ReferenceQueue<Object> queue = new ReferenceQueue<>(); // 添加鍵值對 public V put(K key, V value) { ... // 間接 expungeStaleEntries() Entry<K,V>[] tab = getTable(); ... } // 擴容 void resize(int newCapacity) { // 間接 expungeStaleEntries() Entry<K,V>[] oldTable = getTable(); ... } // 獲取鍵值對 public V get(Object key) { ... // 間接 expungeStaleEntries() Entry<K,V>[] tab = getTable(); ... } private Entry<K,V>[] getTable() { // 清理無效鍵值對 expungeStaleEntries(); return table; } // ->清理無效鍵值對 private void expungeStaleEntries() { // 遍歷引用隊列 for (Object x; (x = queue.poll()) != null; ) { // 疑問 3:既然 WeakHashMap 不考慮線程同步,為什麼這裡要做加鎖,豈不是突兀? synchronized (queue) { Entry<K,V> e = (Entry<K,V>) x; // 根據散列值定位數組下標 int i = indexFor(e.hash /*散列值*/, table.length); // 遍歷桶尋找節點 e 的前驅結點 Entry<K,V> prev = table[i]; Entry<K,V> p = prev; while (p != null) { Entry<K,V> next = p.next; if (p == e) { // 刪除節點 e if (prev == e) // 節點 e 是根節點 table[i] = next; else // 節點 e 是中間節點 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; } } } }
總結
1、WeakHashMap 使用與 Java 7 HashMap 相同的 “數組 + 鏈表” 解決散列沖突,發生散列沖突的鍵值對會用頭插法添加到單鏈表中;
2、WeakHashMap 能夠實現 “自動清理” 的內存緩存,其中的 “Weak” 指鍵 Key 是弱引用。當 Key 對象不再被持有強引用時,垃圾收集器會按照弱引用策略自動回收 Key 對象,並在下次訪問 WeakHashMap 時清理全部無效的鍵值對;
3、WeakHashMap 和 LinkedHashMap 都具備 “自動清理” 的 能力,WeakHashMap 根據 Key 對象的可達性淘汰數據,而 LinkedHashMap 根據 FIFO 或 LRU 策略嘗試淘汰數據;
4、WeakHashMap 使用 Key 弱引用,會存在重建 Key 對象不等價問題。
以上就是WeakHashMap 和 HashMap 的區別是什麼,何時使用的詳細內容,更多關於WeakHashMap HashMap區別的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Java WeakHashMap案例詳解
- java編程Reference核心原理示例源碼分析
- Java面試必問之ThreadLocal終極篇分享
- 基於ThreadLocal 的用法及內存泄露(內存溢出)
- 詳解Java中的ThreadLocal