数据结构 - 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垃圾回收原理

WeakHashMap 是不是有 java.util.concurrent 等价物?

Java中关于WeakReference和WeakHashMap的理解

Java中关于WeakReference和WeakHashMap的理解

Java WeakHashMap何时清除null键?