WeakHashMap源码解读

1. 简介

本文基于JDK8u111的源码分析WeakHashMap的一些主要方法的实现。

2. 数据结构

就数据结构来说WeakHashMap与HashMap原理差不多,都是拉链法来解决哈希冲突。
下面是WeakHashMap中的Entry结构定义。

/**
  * 省略部分方法实现。
  */
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的弱引用,定义如下。

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

这个queue会作为Entry构造方法的一个参数用于实例化WeakReference,其主要作用是为方便清理WeakHashMap中无效Entry。

3. 重要方法源码

3.1 哈希算法

首先看一下WeakHashMap是如何去hash一个Object的

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方法已经简化为只需要一次扰动亦或。

3.2 插入元素

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是如何清理脏数据的

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

3.2.1 扩容机制

与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元素时哪些情况下会清理元素

  • put方法开始的getTable会调用一次expungeStaleEntries
  • 需要扩容时resize方法开始的getTable会调用一次expungeStaleEntries
  • transfer方法本身会判断弱引用指向的对象是否已经被GC
  • 扩容后发现size小于阈值一半,会调用一次expungeStaleEntries

3.3 取出元素

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的清理。

3.3 删除元素

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

4. WeakHashMap的使用

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的场景还是比较少见的。

5. 总结

下面总结一下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中方法实现有优化 自带无效数据清理
posted @ 2018-02-11 20:45  活在夢裡  阅读(827)  评论(0编辑  收藏  举报