环境和版本
- Mac M1
- IDEA 2021
-
Mark*
有关于WeakHashMap的部分,会在后续JUC或者JVM部分进行再次对比讲解。
简介
WeakHashMap,从名字可以看出它是某种 Map。它的特殊之处在于 WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法
更直观的说,当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况
调用两次size()方法返回不同的值; 两次调用isEmpty()方法,第一次返回false,第二次返回true; 两次调用containsKey()方法,第一次返回true,第二次返回false,尽管两次使用的是同一个key; 两次调用get()方法,第一次返回一个value,第二次返回null,尽管两次使用的是同一个对象。
WeakHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到
- 要明白 WeakHashMap 的工作原理,还需要引入一个概念 : 弱引用(WeakReference)。我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的有效引用 并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收
WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢?将一对key, value放入到 WeakHashMap 里并不能避免该key值被GC回收,除非在 WeakHashMap 之外还有对该key的强引用
构造函数
```java public WeakHashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Initial Capacity: "+initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load factor: "+ loadFactor);int capacity = 1; while (capacity < initialCapacity)
capacity <<= 1;table = newTable(capacity); this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); }
public WeakHashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
public WeakHashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
public WeakHashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAll(m); }
<a name="k0Wm2"></a>
# Weak的关键点
- WeakHashMap的Entry继承自WeakReference
```java
// ReferenceQueue
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// Hash表
Entry<K,V>[] table;
// Entry继承了WeakReference
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
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;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
K k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
V v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public int hashCode() {
K k = getKey();
V v = getValue();
return Objects.hashCode(k) ^ Objects.hashCode(v);
}
public String toString() {
return getKey() + "=" + getValue();
}
}
put方法
private static final Object NULL_KEY = new Object();
public V put(K key, V value) {
Object k = maskNull(key);
// 计算hash
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
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++;
// 拿到 tab[i]
// WeakHashMap 没有树化,其是拉链法
Entry<K,V> e = tab[i];
tab[i] = new Entry<>(k, value, queue, h, e);
// 扩容
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
final int hash(Object k) {
int h = k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 获取当前的 table
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}
// 核心方法
// 对死对象进行清理
// 因此,这里的引用处理并不是自动的,其实是我们在调用某些方法的时候处理,所以我们认为它不是一种自动的,只是表面上看起来是这种处理
private void expungeStaleEntries() {
//
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
// 计算桶下标
int i = indexFor(e.hash, table.length);
// 取出下标 prev
Entry<K,V> prev = table[i];
// 赋值p为prev
Entry<K,V> p = prev;
// 只要 p != null
while (p != null) {
// 取出 p 的下一个
Entry<K,V> next = p.next;
// 如果 p的下一个 == e
if (p == e) {
// 如果 prev == e
if (prev == e)
// table[i] 给 next
table[i] = next;
else
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;
}
}
}
}
- A:使用一个继承于WeakReference的entry对象表示每一个kv对,其中的原引用对象即我们在放入map中的key值
- B:为保证效率以及尽可能的不使用key值,hash经过预先计算。这样在定位数据及重新get时不再需要使用原引用对象
- C:由queue拿到的事件对象,即这里的entry值。通过entry定位到具体的桶位置,通过链表计算将entry的前后重新连接起来(即p.pre.next = p.next)
在每个WeakHashMap都有个ReferenceQueue queue,在Entry初始化的时候也会将queue传给WeakReference,这样当某个可以key失去所有强应用之后,其key对应的WeakReference对象会被放到queue里,有了queue就知道需要清理哪些Entry里。这里也是整个WeakHashMap里唯一加了同步的地方。 除了上文说的到resize中调用了expungeStaleEntries(),size()中也调用了这个清理方法。另外 getTable()也调了,这就意味着几乎所有其他方法都间接调用了清理。
get方法
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]; while (e != null) { if (e.hash == h && eq(k, e.get())) return e.value; e = e.next; } return null; }总结
非线程安全:关键修改方法没有任何同步,多线程环境下会有数据不一致的情况,所以使用的时候需要注意。
- 不能自定义ReferenceQueue:WeakHashMap构造方法中没法指定自定的ReferenceQueue,如果用户想用ReferenceQueue做一些额外的清理工作的话就行不通了。如果即想用WeakHashMap的功能,也想用ReferenceQueue,貌似得自己实现一套新的WeakHashMap了
- 单纯作为Map没有HashMap好:HashMap在Jdk8做了好多优化,比如单链表在过长时会转化为红黑树,降低极端情况下的操作复杂度。但WeakHashMap没有相应的优化,有点像jdk8之前的HashMap版本。
其内部的实现原理,其实和之前我们分析的其他源码类似,所以只要过一遍就知道是啥意思了,所以此处省略哪些部分。
参考文章
ReferenceQueue的使用:https://www.cnblogs.com/dreamroute/p/5029899.html
- WeakHashMap:https://www.pdai.tech/md/java/collection/java-map-WeakHashMap.html
