本文已收录到 AndroidFamily,技能和职场问题,请关注公众号 [彭旭锐] 发问。
前言
大家好,我是小彭。
在之前的文章里,咱们聊到了 Java 规范库中 HashMap 与 LinkedHashMap 的完成原理。HashMap 是一个规范的散列表数据结构,而 LinkedHashMap 是在 HashMap 的基础上完成的哈希链表。
今日,咱们来讨论 WeakHashMap,其间的 “Weak” 是指什么,与前两者的运用场景有何不同?咱们就环绕这些问题展开。
提示: 本文源码依据 JDK 1.2 LinkedHashMap。
思想导图:
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 示意图
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;
}
// 未重写 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 的弱引证。
引证关系示意图
不出意外的话又有小朋友出来举手发问了♀️:
- ♀️疑问 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天后未获授权制止转载,侵权必究!