探索WeakHashMap底层实现
前言
探索WeakHashMap底层实现是基于JDK1.8,它的数据结构是数组 + 链表。就不贴它的注释了,直接总结一下吧:
WeakHashMap基于
弱键实现了Map接口,也就是说,当某个键不在使用时会被丢弃,对应的键值对将会被自动移除。如何确定不在使用取决于GC是否运行,而对于GC何时运行我们并不知道,所以某个键何时被丢弃我们也不得而知,至于GC如何运行就是另外一个话题了,有可能导致上一分钟与下一分钟获取到的结果是不一致的。另一个方面,WeakHashMap的值对象由强引用所持有(何为强引用下面会介绍),应确保值对象不会直接或间接引用自身的键或其他键,这会导致键无法被丢弃。
-
强引用:简单来说指向new出来的对象就是一个强引用,可以说是经常使用。对于强引用来说,它们不会被GC回收,即使内存空间不足,JVM宁愿抛出内存溢出错误也不敢动它们,总体来说还是很有威信的。
-
软引用:首先给强引用包裹上一层
SoftReference,通过SoftReference获取到的引用即为软引用。对于软引用来说,在内存充足的情况下,GC可以选择性的清除,而一旦内存不足了,它们一个都跑不了,都会被清除掉。软引用最常用用于实现对内存敏感的缓存。 -
弱引用:首先给强引用包裹上一层
WeakReference,通过WeakReference获取到的引用即为弱引用,看到这里你应该就已经明白了WeakHashMap内部的机制。对于弱引用来说,GC压根就不管内存是否充足,直接回收,很没有人性! -
虚引用:首先给强引用包裹上一层
PhantomReference,通过PhantomReference获取到的引用即为虚引用。对于虚引用来说,它在任何时候都可能被回收,常用于跟踪对象。
还有一个方面,读者最好去了解下Reference类,内部通过队列实现了一些机制。
数据结构
前奏都准备好了,开始进入正题吧。
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
/**
* 默认初始容量,必须是2的幂次方,可参考HashMap
*/
private static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 最大容量,必须是2的幂次方
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子
*/
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 哈希表,长度必须是2的幂次方
*/
Entry<K,V>[] table;
/**
* 哈希表中包含节点的个数
*/
private int size;
/**
* 扩容前需要判断的阈值
* 若超过该值则扩容,若没超过则不需要
* 该值的计算方式:capacity * load factor
*/
private int threshold;
/**
* 加载因子
*/
private final float loadFactor;
/**
* 引用队列
*
* 为什么需要引用队列呢?
* 通过上面的介绍我们可以知道哈希表中某些键可能会被移除掉,而移除是GC帮我们做的,那WeakHashMap怎么知道哪些键被移除掉了以便更新自己的键值对,就是该队列做了它们两个之间的媒介
* 上面让读者去了解Reference类,下面讲的内容其实都在该类中有提到,比较简单
* GC在丢弃某个键时会将它的键值对,也就是节点信息存放到Reference类中的pending队列中,Reference类在初始化时会启动一个线程,那么该线程会将pending队列中的节点信息放入到queue队列中
* 也就是在告诉WeakHashMap,队列中的这些节点是我要删除的,你记得更新
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/**
* 缓存entrySet方法的返回值
*/
private transient Set<Map.Entry<K,V>> entrySet;
/**
* 结构修改的次数
*/
int modCount;
}
构造函数
/**
* 指定初始容量与加载因子构造哈希表
* 在上面中我们提到了容量必须是2的幂次方,所以调用tableSizeFor方法来进行调整
* Float.isNaN:检测是否是数字
* @param initialCapacity 指定初始容量
* @param loadFactor 指定加载因子
*/
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) //这段代码有点精髓啊,个人感觉比HashMap中的算法简单,两者要表达的意思是一致的,都是获取大于initialCapacity的最小值
capacity <<= 1;
table = newTable(capacity);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
}
/**
* 指定初始容量与默认加载因子(0.75)构造哈希表
* @param initialCapacity 指定初始容量
*/
public WeakHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 默认初始容量(16)与默认加载因子(0.75)构造哈希表
*/
public WeakHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* 将指定集合添加到哈希表中,采用默认加载因子
* @param m 指定集合
*/
public WeakHashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);//Math.max是为了获取尽可能大的容量
putAll(m);
}
简单方法
/**
* 根据指定长度构造哈希表
* @param n 指定长度
* @return 哈希表
*/
@SuppressWarnings("unchecked")
private Entry<K,V>[] newTable(int n) {
return (Entry<K,V>[]) new Entry<?,?>[n];
}
/**
* 倘若键为null则采用NULL_KEY作为键
* 正如方法名一样,隐藏Null
* @param key 指定键
* @return NULL_KEY或指定键
*/
private static Object maskNull(Object key) {
return (key == null) ? NULL_KEY : key;
}
/**
* 倘若键为NULL_KEY则返回null
* 正如方法名一样,揭露Null
* @param key 哈希表中的键
* @return null或指定键
*/
static Object unmaskNull(Object key) {
return (key == NULL_KEY) ? null : key;
}
/**
* 两个对象是否相等
* @param x 对象
* @param y 另外一个对象
* @return 是否相等
*/
private static boolean eq(Object x, Object y) {
return x == y || x.equals(y);
}
/**
* 计算哈希值
* 这边的计算哈希值比HashMap复杂多了,涉及到算法的内容我感觉我没办法理解到位
* @param k 对象
* @return 哈希值
*/
final int hash(Object k) {
int h = k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 计算哈希表中的索引
* @param h 哈希值
* @param length 哈希表的长度
* @return 索引
*/
private static int indexFor(int h, int length) {
return h & (length-1);
}
/**
* 清除哈希表中过时的节点信息
* 过时指的是已经被丢弃的键,也可以说是被GC回收的键
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {//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;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e) //说明当前节点是链表的首部节点
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;
}
}
}
}
/**
* 获取哈希表
* @return 哈希表
*/
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}
/**
* 获取哈希表的长度
* @return 哈希表的长度
*/
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();
return size;
}
/**
* 哈希表是否为空
* @return 哈希表是否为空
*/
public boolean isEmpty() {
return size() == 0;
}
/**
* 指定键获取指
* @param key 指定键
* @return null或值
*/
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;
}
/**
* 是否包含指定键
* @param key 指定键
* @return 是否包含指定键
*/
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
/**
* 指定键获取节点
* @param key 指定键
* @return null或节点
*/
Entry<K,V> getEntry(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 && !(e.hash == h && eq(k, e.get())))
e = e.next;
return e;
}
/**
* 新增节点
* 链表中采用头插法的方式进行新增节点
* 若超过阈值则会进行扩容
* @param key 指定键
* @param value 指定值
* @return null或旧值
*/
public V put(K key, V value) {
Object k = maskNull(key);
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++;
Entry<K,V> e = tab[i];
tab[i] = new Entry<>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
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;
/**
* 如果忽略null元素并处理队列导致大量收缩,则还原旧表。 这应该很少见,但是可以避免持有大量无用节点的哈希表的无限扩展。
*/
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else { //GC回收了大量的节点后则不进行扩容
expungeStaleEntries(); //检测新表中哪些节点已经被丢弃了
transfer(newTable, oldTable);
table = oldTable;
}
}
/**
* 将源哈希表中的所有节点信息复制到目标哈希表中
* 源哈希表中可能出现被丢弃的键
* @param src 源哈希表
* @param dest 目标哈希表
*/
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(); //若当前节点已经被GC回收了,则此方法返回将返回null
if (key == null) {
e.next = null; // Help GC
e.value = null; // " "
size--;
} else {
int i = indexFor(e.hash, dest.length); //该索引出现的可能应该跟HashMap是一样的,原索引或与原索引 + 旧容量的大小,只不过它是一个一个的计算并添加,而HashMap是分批计算,一次性添加
e.next = dest[i];
dest[i] = e;
}
e = next;
}
}
}
/**
* 批量添加节点到哈希表中
* @param m 集合
*/
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
/**
* 倘若指定集合的键值对数量超过阈值则进行扩容. 这是保守的;
* 很明显的条件应该是 (m.size + size) >= threshold, 但是这个条件会导致适当的容量变成2倍,如果被添加的键已经存在于哈希表中.
* 通过使用保守的计算,我们最多只能调整一种尺寸。
*/
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length) //预先计算好要添加节点的数量以便进行一次性扩容
resize(newCapacity);
}
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
/**
* 指定键移除节点
* @param key 指定键
* @return null或值
*/
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;
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;
}
/**
* 指定键移除节点是否成功
* @param o 指定键
* @return 移除节点是否成功
*/
boolean removeMapping(Object o) {
if (!(o instanceof Map.Entry))
return false;
Entry<K,V>[] tab = getTable();
Map.Entry<?,?> entry = (Map.Entry<?,?>)o;
Object k = maskNull(entry.getKey());
int h = hash(k);
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;
if (h == e.hash && e.equals(entry)) {
modCount++;
size--;
if (prev == e)
tab[i] = next;
else
prev.next = next;
return true;
}
prev = e;
e = next;
}
return false;
}
/**
* 清空哈希表
*/
public void clear() {
while (queue.poll() != null) //清空队列中只有一部分过时节点
;
modCount++;
Arrays.fill(table, null); //清空哈希表后
size = 0;
/**
* 清空哈希表后可能导致GC,另外一部分节点会被添加到队列中,所以此处需要再次清空队列
*/
while (queue.poll() != null)
;
}
/**
* 哈希表中是否包含指定值
* @param value 指定值
* @return 是否包含指定值
*/
public boolean containsValue(Object value) {
if (value==null)
return containsNullValue();
Entry<K,V>[] tab = getTable();
for (int i = tab.length; i-- > 0;)
for (Entry<K,V> e = tab[i]; e != null; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
/**
* 哈希表中是否包含null值
* @return 是否包含null值
*/
private boolean containsNullValue() {
Entry<K,V>[] tab = getTable();
for (int i = tab.length; i-- > 0;)
for (Entry<K,V> e = tab[i]; e != null; e = e.next)
if (e.value==null)
return true;
return false;
}
/**
* 哈希表中的节点,该类继承了WeakReference加上调用了父类的构造,说明它的键是个弱引用
* 该类中的其他方法就不做展示了,比较简单
*/
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* 初始化
* 指定键生成弱引用
* @param key 指定键
* @param value 指定值
* @param queue 与弱引用关联的队列
* @param hash 哈希值
* @param 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;
}
}
/**
* 遍历所有键并执行指定动作
* 遍历过程中不允许WeakHashMap调用任何会修改结构的方法,否则最后会抛出异常
* @param action 指定动作
*/
public void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
int expectedModCount = modCount;
Entry<K, V>[] tab = getTable();
for (Entry<K, V> entry : tab) {
while (entry != null) {
Object key = entry.get();
if (key != null) {
action.accept((K)WeakHashMap.unmaskNull(key), entry.value);
}
entry = entry.next;
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
}
}
}
/**
* 遍历哈希表并执行指定动作后获取新值,利用新值替换所有节点的旧值
* @param function 指定动作
*/
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
int expectedModCount = modCount;
Entry<K, V>[] tab = getTable();;
for (Entry<K, V> entry : tab) {
while (entry != null) {
Object key = entry.get();
if (key != null) {
entry.value = function.apply((K)WeakHashMap.unmaskNull(key), entry.value);
}
entry = entry.next;
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
}
}
}
//一些重复性的东西,比如包含键、值、键值对的迭代器、可分割迭代器就不讲解了,可参考HashMap
总结
-
WeakHashMap的键值对允许为null。
-
WeakHashMap采用弱键,当某个键不在使用时会被GC回收,而键对应的节点也会被移除掉。
-
WeakHashMap无序不可重复、非线程安全。
-
在添加节点,值对象最好不要与任何的键直接或间接的关联,否则GC无法丢弃该键。
-
WeakHashMap#ReferendeQueue是用来查看哈希表中哪些键被丢弃了,以便哈希表能够及时更新。
-
WeakHashMap的容量必须是2的幂次方。
-
WeakHashMap在新增节点时采用的是头插法。
重点关注
弱键 ReferenceQueue 头插法 强、软、弱、虚引用 Reference
浙公网安备 33010602011771号