WeakHashMap源码解读

时间:2023-03-10 07:09:05
WeakHashMap源码解读

1. 简介

本文基于JDK8u111的源码分析WeakHashMap的一些主要方法的实现。

2. 数据结构

就数据结构来说WeakHashMap与HashMap原理差不多,都是拉链法来解决哈希冲突。

下面是WeakHashMap中的Entry结构定义。

/**
* 省略部分方法实现。
*/
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
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, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
@SuppressWarnings("unchecked")
public K getKey() {
return (K) WeakHashMap.unmaskNull(get());
}
public V getValue() {
return value;
} public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} }

另外,每个WeakHashMap内部都有一个ReferenceQueue用于收集被GC的弱引用,定义如下。

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

这个queue会作为Entry构造方法的一个参数用于实例化WeakReference,其主要作用是为方便清理WeakHashMap中无效Entry。

3. 重要方法源码

3.1 哈希算法

首先看一下WeakHashMap是如何去hash一个Object的

final int hash(Object k) {
int h = k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

可以看到WeakHashMap的hash方法实际上是和JDK7中HashMap是相同的。

因为WeakHashMap与HashMap类似,Capacity也是2的幂。如果直接用对象的hashCode那么在计算bucket的index的时候可能会出现比较严重的冲突(高位不同,低位相同分配到同一个bucket中)。为了避免这种情况,需要将高位与低位作一个混淆或者扰动,增加bucket index的随机性。

在JDK8的HashMap类中,hash方法已经简化为只需要一次扰动亦或。

3.2 插入元素

public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k); // getTable会作一次清理。
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length); // 遍历bucket中元素,查询是否命中map中已有元素。
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];
// 将新元素插入到bucket中。
tab[i] = new Entry<>(k, value, queue, h, e); // 超过阈值后扩容一倍。
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}

下面来看看WeakHashMap是如何清理脏数据的

private void expungeStaleEntries() {
// 遍历该WeakHashMap的reference queue中被回收的弱引用。
for (Object x; (x = queue.poll()) != null; ) {
/*
* 这里有个值得注意的点就是下面的代码被包在queue的同步块中。
* 因为这里不同步的话,WeakHashMap在不涉及修改,只有并发读的情况下,
* 下面的清理在多线程情况下可能会破坏内部数据结构。
* 而之所以不在整个方法级别作同步,原因是上面的ReferenceQueue的poll方法是线程安全,
* 可以并发取数据的(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;
// 遍历对应bucket中的元素。
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
// 意味着table[i]==e,直接将table[i]向后指一位即可
if (prev == e)
table[i] = next;
else
// 删除p节点,将前驱和后继链接上。
prev.next = next;
// 因为可能有HashIterator正在遍历,所以e.next这里不清为null。
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

3.2.1 扩容机制

与HashMap类似,在WeakHashMap中元素超过阈值threshold时也会发生扩容,下面是WeakHashMap的resize方法实现

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; /*
* WeakHashMap这里有个很严谨的设计是会再次判断元素个数是否超过阈值的一半
* 因为在刚开始getTable以及后续transfer过程中都有清理机制(transfer方法不会去拷贝已经被回收的元素)。
* 如果size的值小于阈值的一半,为了避免WeakHashMap的Capacity的无限扩张,会去重新将元素拷贝到原先的数组中。
*/
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
expungeStaleEntries();
transfer(newTable, oldTable);
table = oldTable;
}
} 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();
if (key == null) {
e.next = null;
e.value = null;
size--;
} else {
int i = indexFor(e.hash, dest.length);
e.next = dest[i];
dest[i] = e;
}
e = next;
}
}
}

至此,put方法我们已经阅读的差不多了。这里梳理一下WeakHashMap在我们操作put元素时哪些情况下会清理元素

  • put方法开始的getTable会调用一次expungeStaleEntries
  • 需要扩容时resize方法开始的getTable会调用一次expungeStaleEntries
  • transfer方法本身会判断弱引用指向的对象是否已经被GC
  • 扩容后发现size小于阈值一半,会调用一次expungeStaleEntries

3.3 取出元素

WeakHashMap根据key获取一个mapping对应的value还是相对比较简单的。

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];
// 遍历bucket中元素。
while (e != null) {
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}

可以看到在get方法中也有getTable方法的调用,这里也会涉及到已被GC的key对应entry的清理。

3.3 删除元素

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;
/*
* 这里的逻辑其实和expungeStaleEntries类似,
* 如果在bucket最外的端点,则直接把tab[i]的指向往后面挪一下即可,
* 否则将待删除节点前驱和后继链接上即可。
*/
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;
}

4. WeakHashMap的使用

WeakHashMap的一种使用场景是不影响key生命周期的缓存。可以参考tomcat中的ConcurrentCache中,使用了WeakHashMap。我们来看下代码:

public final class ConcurrentCache<K,V> {

    private final int size;

    private final Map<K,V> eden;

    private final Map<K,V> longterm;

    public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
} public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
synchronized (longterm) {
v = this.longterm.get(k);
}
if (v != null) {
this.eden.put(k, v);
}
}
return v;
} public void put(K k, V v) {
if (this.eden.size() >= size) {
synchronized (longterm) {
this.longterm.putAll(this.eden);
}
this.eden.clear();
}
this.eden.put(k, v);
}
}

不过在我实际项目开发中,一般碰到需要用到WeakHashMap的场景还是比较少见的。

5. 总结

下面总结一下WeakHashMap,并和HashMap以及ThreadLocalMap作一个比较。

比较内容 WeakHashMap HashMap ThreadLocalMap
存储方式 拉链法 拉链法 开放地址法
存储数据 任意,key/value可为null,实际存储的Entry为key的弱引用。 任意,key/value可为null key为ThreadLocal对象,value任意类型可为null,key一定不会为null(没法自己塞null),实际存储的Entry是key的弱引用。
对key的GC影响 Entry为弱引用,不影响key的GC 强引用,对key的GC有影响 Entry为弱引用,不影响key的GC
线程安全
其它 自带无效数据清理 JDK8中方法实现有优化 自带无效数据清理