探索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

posted @ 2020-12-21 19:23  zliawk  阅读(96)  评论(0)    收藏  举报