本文基于JDK8u111的源码分析WeakHashMap的一些主要方法的实现。java
就数据结构来讲WeakHashMap与HashMap原理差很少,都是拉链法来解决哈希冲突。
下面是WeakHashMap中的Entry结构定义。git
/** * 省略部分方法实现。 */ 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的弱引用,定义以下。github
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
这个queue会做为Entry构造方法的一个参数用于实例化WeakReference,其主要做用是为方便清理WeakHashMap中无效Entry。算法
首先看一下WeakHashMap是如何去hash一个Object的apache
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方法已经简化为只须要一次扰动亦或。缓存
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是如何清理脏数据的tomcat
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; } } } }
与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元素时哪些状况下会清理元素数据结构
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的清理。
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; }
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的场景仍是比较少见的。
下面总结一下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中方法实现有优化 | 自带无效数据清理 |