本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 提問。
大家好,我是小彭。
在之前的文章裡,我們聊到了 Java 標準庫中 HashMap 與 LinkedHashMap 的實現原理。HashMap 是一個標準的雜湊表資料結構,而 LinkedHashMap 是在 HashMap 的基礎上實現的雜湊連結串列。
今天,我們來討論 WeakHashMap,其中的 「Weak」 是指什麼,與前兩者的使用場景有何不同?我們就圍繞這些問題展開。
提示: 本文原始碼基於 JDK 1.2 WeakHashMap。
小彭的 Android 交流群 02 群已經建立啦,掃描文末二維條碼進入~
思維導圖:
其實,WeakHashMap 與 HashMap 和 LinkedHashMap 的資料結構大同小異,所以我們先回顧後者的實現原理。
HashMap 是基於分離連結串列法解決雜湊衝突的動態雜湊表。
HashMap 實現示意圖
LinkedHashMap 是繼承於 HashMap 實現的雜湊連結串列。
LinkedHashMap 示意圖
WeakHashMap 中的 「Weak」 指鍵 Key 是弱參照,也叫弱鍵。弱參照是 Java 四大參照型別之一,一共有四種參照型別,分別是強參照、軟參照、弱參照和虛參照。我將它們的區別概括為 3 個維度:
感知垃圾回收示意圖
提示: 關於 「Java 四種參照型別」 的區別,在小彭的 Java 專欄中深入討論過 《說一下 Java 的四種參照型別》,去看看。
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 示意圖
自動清理資料
WeakHashMap 與 HashMap 都是基於分離連結串列法解決雜湊衝突的動態雜湊表,兩者的主要區別在 鍵 Key 的參照型別上:
HashMap 會持有鍵 Key 的強參照,除非手動移除,否則鍵值對會長期存在於雜湊表中;
WeakHashMap 只持有鍵 Key 的弱參照,當 Key 物件不再被外部持有強參照時,鍵值對會被自動被清理。
WeakHashMap 與 LinkedHashMap 都有自動清理的能力,兩者的主要區別在於 淘汰資料的策略上:
LinkedHashMap 會按照 FIFO 或 LRU 的策略 「嘗試」 淘汰資料,需要開發者重寫 removeEldestEntry()
方法實現是否刪除最早節點的判斷邏輯;
WeakHashMap 會按照 Key 物件的可達性淘汰資料,當 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);
}
不管是 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;
}
// 未重寫 equals
}
這一節,我們來分析 WeakHashMap 中主要流程的原始碼。
事實上,WeakHashMap 就是照著 Java 7 版本的 HashMap 依葫蘆畫瓢的,沒有樹化的邏輯。考慮到我們已經對 HashMap 做過詳細分析,所以我們沒有必要重複分析 WeakHashMap 的每個細節,而是把重心放在 WeakHashMap 與 HashMap 不同的地方。
先用一個表格整理 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> {
// Key:與 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 的弱參照。
參照關係示意圖
不出意外的話又有小朋友出來舉手提問了