数据结构 - WeakHashMap
Posted yuanjiangnan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构 - WeakHashMap相关的知识,希望对你有一定的参考价值。
简介
WeakHashMap的键是弱键。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
? ? 什么是弱键?弱键是通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是弱键,即是WeakReference
类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:
1、新建WeakHashMap,将“键值对”添加到WeakHashMap中。实际上,WeakHashMap是通过数组槽保存Entry键值对;
每一个Entry实际上是一个单向链表
2、当某弱键不再被其它对象引用,并被GC回收时。在GC回收该弱键时,这个弱键也同时会被添加到ReferenceQueue(queue)
队列中
3、当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC
回收的键值对;同步它们,就是删除table中被GC回收的键值对
WeakHashMap 类
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
注意它不继承HashMap,但是和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射
WeakHashMap属性
// 默认槽个数
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 默认最大槽个数
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
// hash数组(槽数组)
Entry<K,V>[] table;
// 元素个数
private int size;
// 扩容阈值
private int threshold;
// 实际加载因子
private final float loadFactor;
// 引用队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 修改次数
int modCount;
WeakHashMap构造函数
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);
// 这儿的意思是找到一个数,大于等于initialCapacity并且是2的幂
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
table = newTable(capacity);
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
}
注意这里跟HashMap不同,初始化时就会创建数组,计算2的n次幂跟HashMap也不同
while (capacity < initialCapacity)
capacity <<= 1;
这种方式比较消耗性能,但是WeakHashMap的key一直在回收,初始化时一般会太大,所以性能损耗在可接受范围内
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);
}
WeakHashMap 的hash方法
final int hash(Object k) {
int h = k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
我们知道HashMap的hash方法 h ^ (h >>> 16),为了解决高位不能参与取余,那WeakHashMap为什么搞这么麻烦呢?首先要明白WeakHashMap的数组一定不会特别多,因为GC会把键回收掉,所以实际参与取余的位数就更少了,这么做的目的就是让整个hashcode充分参与取余
WeakHashMap 键取余
private static int indexFor(int h, int length) {
return h & (length-1);
}
WeakHashMap 添加
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);
// 先找槽中头结点
for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
// 有重复的key用新值覆盖旧值
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];
// 创建新的Entry
tab[i] = new Entry<>(k, value, queue, h, e);
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
从put方法可以看出,它没有红黑树,只是简单的链表
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}
源代码中很多地方都会调用expungeStaleEntries()清除已经被回收的键
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;
// 遍历槽中的元素
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;
}
}
}
}
expungeStaleEntries方法会显得有些突兀,主要原因是不了解WeakReference弱键,这里我普及一下WeakReference和ReferenceQueue
从JDK 1.2版本开始,把对象的引用分为4种级别
1、强引用(StrongReference)?
? ? 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。?
2、软引用(SoftReference)?
? ? 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。?
3、弱引用(WeakReference)?
? ? 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。?
4、虚引用(PhantomReference)?
? ? 虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
现在我们知道,GC回收弱引用后会把引用指向的对象丢到ReferenceQueue中,所以只要key被回收,上面queue就会有值,expungeStaleEntries方法的作用就是,key被gc回收后置空key对应的对象,并且重新整理链表。
WeakHashMap 扩容
void resize(int newCapacity) {
// 获取原Entry数组,获取之前先删除所有需要被移除的Entry
Entry<K,V>[] oldTable = getTable();
// 获取原Entry数组的容量
int oldCapacity = oldTable.length;
// 如果原数组的容量已经达到最大值2^30,则停止扩容并防止再次扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 实例化新Entry数组
Entry<K,V>[] newTable = newTable(newCapacity);
// 将原数组中的元素全部放入新数组
transfer(oldTable, newTable);
// 将底层数组替换为新数组
table = newTable;
// 如果忽略空元素并处理ref队列导致大量收缩,则恢复旧表。
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
expungeStaleEntries();
transfer(newTable, oldTable);
table = oldTable;
}
}
WeakHashMap元素迁移
private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
// 遍历src数组
for (int j = 0; j < src.length; ++j) {
// 依次将src数组中的元素取出
Entry<K,V> e = src[j];
// 取出后将src中去除引用
src[j] = null;
// 遍历链表
while (e != null) {
// 取当前节点的下一节点next
Entry<K,V> next = e.next;
// 获取弱键
Object key = e.get();
// 弱键为空则说明已被GC回收,将该节点清空,方便GC回收该Entry
if (key == null) {
e.next = null; // Help GC
e.value = null; // " "
size--;
} else {
// 不为空说明没有被回收,重新计算在dest数组中的索引
int i = indexFor(e.hash, dest.length);
// 将该节点插入dest索引i处链表头
e.next = dest[i];
dest[i] = e;
}
e = next;
}
}
}
从源码中可以看出,他的扩容可能会取消,跟HashMap不同,接下来看看删除方法
WeakHashMap 删除
public V remove(Object key) {
//检查key为null则返回NULL_KEY对象
Object k = maskNull(key);
//取key的hashCode
int h = hash(k);
//获取Entry数组,获取之前先删除所有需要被移除的Entry
Entry<K,V>[] tab = getTable();
//计算出该key的下标索引
int i = indexFor(h, tab.length);
//取索引i处的Entry,用来遍历链表时存放上个节点
Entry<K,V> prev = tab[i];
//用来遍历链表时存放当前节点
Entry<K,V> e = prev;
//遍历链表
while (e != null) {
//取当前节点的下一节点next
Entry<K,V> next = e.next;
//根据hashCode值与equals比较双重判断key是否相同,存在则移除该节点并返回该节点value
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 清空
public void clear() {
//清空queue,将queue中的元素一个个取出。这是神马骚操作,长见识了!!!
while (queue.poll() != null)
;
modCount++;
//通过Arrays.fill把底层数组所有元素全部清空
Arrays.fill(table, null);
size = 0;
//将数组清空后可能引发了GC导致queue中又添加了元素,再次清空
while (queue.poll() != null)
;
}
清空方法比较特殊,主要就是清空queue,下面是get方法
WeakHashMap 键取值
public V get(Object key) {
//检查key为null则返回NULL_KEY对象
Object k = maskNull(key);
//取key的hashCode
int h = hash(k);
//通过getTable获取数组,去除所有需要被回收的Entry
Entry<K,V>[] tab = getTable();
//计算出该key的下标索引
int index = indexFor(h, tab.length);
//获取该索引位置的链表头
Entry<K,V> e = tab[index];
//遍历链表,根据hashCode值与equals比较双重判断获取value
while (e != null) {
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}
WeakHashMap 重要内部类
Entry<K,V> 类
private static class Entry<K,V> extends WeakReference<Object>
implements Map.Entry<K,V>
Entry<K,V>为私有内部类,并且继承WeakReference弱引用,WeakReference继承Reference,键的引用实际上是赋给Reference抽象类的属性
Entry<K,V> 属性
// 值
V value;
// 键的hash
final int hash;
// 下一个元素
Entry<K,V> next;
Entry<K,V> 构造函数
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;
}
从构造函数和属性可以看出,Entry<K,V>把键丢给父类去管理了
Entry<K,V> 方法
public K getKey() {
return (K) WeakHashMap.unmaskNull(get());
}
get() 是抽象类Reference的方法,作用是通过引用取值
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);
}
键的hashCode和值的hashCode取异或
以上是关于数据结构 - WeakHashMap的主要内容,如果未能解决你的问题,请参考以下文章
Java 集合系列13之 WeakHashMap详细介绍(源码解析)和使用示例
WeakHashMap 是不是有 java.util.concurrent 等价物?
Java中关于WeakReference和WeakHashMap的理解