本文已收录到 AndroidFamily,技能和职场问题,请关注公众号 [彭旭锐] 发问。

前言

大家好,我是小彭。

在之前的文章里,咱们聊到了 Java 规范库中 HashMap 与 LinkedHashMap 的完成原理。HashMap 是一个规范的散列表数据结构,而 LinkedHashMap 是在 HashMap 的基础上完成的哈希链表。

今日,咱们来讨论 WeakHashMap,其间的 “Weak” 是指什么,与前两者的运用场景有何不同?咱们就环绕这些问题展开。

提示: 本文源码依据 JDK 1.2 LinkedHashMap。


思想导图:

WeakHashMap 和 HashMap 的区别是什么,何时使用?


1. 回忆 HashMap 和 LinkedHashMap

其实,WeakHashMap 与 HashMap 和 LinkedHashMap 的数据结构大同小异,所以咱们先回忆后者的完成原理。

1.1 说一下 HashMap 的完成结构

HashMap 是依据别离链表法处理散列抵触的动态散列表。

  • 1、HashMap 在 Java 7 中运用的是 “数组 + 链表”,产生散列抵触的键值对会用头插法添加到单链表中;
  • 2、HashMap 在 Java 8 中运用的是 “数组 + 链表 + 红黑树”,产生散列抵触的键值对会用尾插法添加到单链表中。假如链表的长度大于 8 时且散列表容量大于 64,会将链表树化为红黑树。

HashMap 完成示意图

WeakHashMap 和 HashMap 的区别是什么,何时使用?

1.2 说一下 LinkedHashMap 的完成结构

LinkedHashMap 是承继于 HashMap 完成的哈希链表。

  • 1、LinkedHashMap 同时具有双向链表和散列表的特点。当 LinkedHashMap 作为散列表时,首要体现出 O(1) 时刻复杂度的查询效率。当 LinkedHashMap 作为双向链表时,首要体现出有序的特性;
  • 2、LinkedHashMap 支撑 FIFO 和 LRU 两种排序形式,默许是 FIFO 排序形式,即依照刺进顺序排序。Android 中的 LruCache 内存缓存和 DiskLruCache 磁盘缓存也是直接复用 LinkedHashMap 供给的缓存管理才能。

LinkedHashMap 示意图

WeakHashMap 和 HashMap 的区别是什么,何时使用?


2. 知道 WeakHashMap

2.1 WeakReference 弱引证的特点

WeakHashMap 中的 “Weak” 指键 Key 是弱引证,也叫弱键。弱引证是 Java 四大引证类型之一,一共有四种引证类型,分别是强引证、软引证、弱引证和虚引证。我将它们的差异归纳为 3 个维度:

  • 维度 1 – 目标可达性状态的差异: 强引证指向的目标是强可达的,只有强可达的目标才会认为是存活的目标,才干确保在废物搜集的过程中不会被收回;
  • 维度 2 – 废物收回战略的差异: 不同的引证类型的收回急进程度不同,
    • 强引证指向的目标不会被收回;
    • 软引证指向的目标在内存足够时不会被收回,在内存不足时会被收回;
    • 弱引证和虚引证指向的目标不管在内存是否足够的时候都会被收回;
  • 维度 3 – 感知废物收回机遇: 当引证目标相关的实践目标被废物收回时,引证目标会进入相关的引证行列,程序能够经过观察引证行列的办法,感知目标被废物收回的机遇。

感知废物收回示意图

WeakHashMap 和 HashMap 的区别是什么,何时使用?

提示: 关于 “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 示意图

WeakHashMap 和 HashMap 的区别是什么,何时使用?

主动整理数据

WeakHashMap 和 HashMap 的区别是什么,何时使用?

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;
    }
    // 未重写 equals
}

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> {
        // 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 的弱引证。

引证关系示意图

WeakHashMap 和 HashMap 的区别是什么,何时使用?

不出意外的话又有小朋友出来举手发问了‍♀️

  • ‍♀️疑问 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 目标已经被收回的无效键值对。其间会遍历 ReferenceQueue 中持有的弱引证目标(即 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;
            }
        }
    }
}

4. 总结

  • 1、WeakHashMap 运用与 Java 7 HashMap 相同的 “数组 + 链表” 处理散列抵触,产生散列抵触的键值对会用头插法添加到单链表中;

  • 2、WeakHashMap 能够完成 “主动整理” 的内存缓存,其间的 “Weak” 指键 Key 是弱引证。当 Key 目标不再被持有强引证时,废物搜集器会依照弱引证战略主动收回 Key 目标,并在下次拜访 WeakHashMap 时整理全部无效的键值对;

  • 3、WeakHashMap 和 LinkedHashMap 都具有 “主动整理” 的 才能,WeakHashMap 依据 Key 目标的可达性筛选数据,而 LinkedHashMap 依据 FIFO 或 LRU 战略尝试筛选数据;

  • 4、WeakHashMap 运用 Key 弱引证,会存在重建 Key 目标不等价问题。


版权声明

本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!


WeakHashMap 和 HashMap 的区别是什么,何时使用?