java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > WeakHashMap源码

Java之WeakHashMap源码浅析

作者:澄风

这篇文章主要介绍了Java之WeakHashMap源码浅析,WeakHashMap从名字可以得知主要和Map有关,不过还有一个Weak,我们就更能自然而然的想到这里面还牵扯到一种弱引用结构,因此想要彻底搞懂,我们还需要知道四种引用,需要的朋友可以参考下

定义

从名字可以得知主要和Map有关,不过还有一个Weak,我们就更能自然而然的想到这里面还牵扯到一种弱引用结构,因此想要彻底搞懂,我们还需要知道四种引用。

源码解析

看下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);
    int capacity = 1;
    // 保证容量是2的整数倍,有助于hash运算
    while (capacity < initialCapacity)
        capacity <<= 1;
    // 初始化table数组
    table = newTable(capacity);
    this.loadFactor = loadFactor;
    // 阀值
    threshold = (int)(capacity * loadFactor);
}

没什么好说的 table 是一个 Entry数组 Entry<K,V>[] table; newTable会初始化一个数组数组的容量就是前面计算出来的capacity,其值为2的整数次方。

HashMap的容量为什么是2的n次方?HashMap是如何保证容量是2的n次方的? HashMap容量取2的n次方,主要与hash寻址有关。在put(key,value)时,putVal()方法中通过i = (n - 1) & hash来计算key的散列地址。其实,i = (n - 1) & hash是一个%操作。也就是说,HashMap是通过%运算来获得key的散列地址的。但是,%运算的速度并没有&的操作速度快。而&操作能代替%运算,必须满足一定的条件,也就是a%b=a&(b-1)仅当b是2的n次方的时候方能成立。这也就是为什么HashMap的容量需要保持在2的n次方了。

再看下Entry的类定义

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;
    }
    ...
}

可以看到Entry是继承WeakReference的,我们结合WeakReference再看一下:

public class WeakReference<T> extends Reference<T> {
    /**
     * Creates a new weak reference that refers to the given object.  The new
     * reference is not registered with any queue.
     * 创建一个新的弱应用给传入的对象,这个新的引用不注册任何队列
     *
     * @param referent object the new weak reference will refer to
     */
    public WeakReference(T referent) {
        super(referent);
    }
    /**
     * Creates a new weak reference that refers to the given object and is
     * registered with the given queue.
     * 创建一个新的弱应用给传入的对象,这个新的引用注册给一个给定的队列
     *
     * @param referent object the new weak reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

我们发现在 weakhashmap 中把key注册给了 WeakReference ,也就是说在 WeakHashMap 中key是一个弱引用。但这个queue是什么我们接着看,在往 WeakHashMap 中put一个元素的时候,会创建Entry。再看 WeakHashMap 的put操作,我们如果熟悉 HashMap 其实我们不需要怎么看这部分的代码,无非是计算hash值,散列分布到数组的各个位置,如果 hash 冲突使用拉链法进行解决。这里和hashmap有一点不一样的是hashmap如果链长达到阀值会使用红黑树。

public V put(K key, V value) {
    // 如果key是null则给定一个空的对象进行修饰
    Object k = maskNull(key);
    // 计算key的hash
    int h = hash(k);
    // 获取table
    Entry<K,V>[] tab = getTable();
    // 根据hash找到数组下标
    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;
}

可以看到这个queue是一个实例化final修饰的属性。

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

再看下getTable是什么情况,看源码会知道所有的WeekHashMap的所有操作都要调用 getTable -> expungeStaleEntries

private Entry<K,V>[] getTable() {
    expungeStaleEntries();
    return table;
}

我们看下expungeStaleEntries做了哪些事情?

private void expungeStaleEntries() {
    // 从 ReferenceQueue中拉取元素
    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);
            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
                    // 拿到entry的值赋值为null帮助GC
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

expungeStaleEntries 就是WeakHashMap的核心了,它承担着Map中死对象的清理工作。原理就是依赖WeakReference和ReferenceQueue的特性。

在每个WeakHashMap都有个ReferenceQueue queue,在Entry初始化的时候也会将queue传给WeakReference,这样当某个可以key失去所有强应用之后,其key对应的WeakReference对象会被放到queue里,有了queue就知道需要清理哪些Entry了。

这里也是整个WeakHashMap里唯一加了同步的地方。除了上文说的到resize中调用了expungeStaleEntries(),size()中也调用了这个清理方法。另外 getTable()也调了,这就意味着几乎所有其他方法都间接调用了清理。

WeakHashMap的一点点缺点

提到缺点我不太认为是缺点,在某种场景下缺点也有可能是优点,而且很多缺点也是可以弥补的。

但非要说个一二三,这里列出下面两种:

1.非线程安全

关键修改方法没有提供任何同步,多线程环境下肯定会导致数据不一致的情况,所以使用时需要多注意。

2.单纯作为Map没有HashMap好

HashMap在Jdk8做了好多优化,比如单链表在过长时会转化为红黑树,降低极端情况下的操作复杂度。但WeakHashMap没有相应的优化,有点像jdk8之前的HashMap版本。

WeakHashMap可以应用的地方

1.缓存

2.诊断工具,比如atlas,将字节码缓存放入到WeakHashMap中

到此这篇关于Java之WeakHashMap源码浅析的文章就介绍到这了,更多相关WeakHashMap源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文