【Java】WeakHashMap源码阅读笔记

本文详细探讨了WeakHashMap的内部实现,包括其特殊属性如ReferenceQueue和Entry<K,V>结构。分析了WeakHashMap如何处理弱引用以及在对象被回收后的自动清理机制。文章通过构造函数、PUT、RESIZE、TRANSFER等关键操作揭示了WeakHashMap的工作原理,并讨论了expungeStaleEntries方法中的疑点,强调理解引用关系对于掌握WeakHashMap的重要性。" 126249786,15061692,便携示波器拆解对比:正点原子DS100 vs 梦源实验室U2P20,"['电子工程', '硬件评测', '示波器选购', '嵌入式开发', '数字电路']
摘要由CSDN通过智能技术生成

HashMap中如果一个键指向的对象没有其他任何引用指向它,也不会被GC回收,被map的桶接管了,存在强引用,只能手动remove。如果想要这种键值对被自动回收怎么办?带着问题看一下WeakHashMap的源码

WeakHashMap的属性

和HashMap中基本一样,不过WeakHashMap没有红黑树相关操作,还多了个ReferenceQueue。

ReferenceQueue存的是什么呢?这里可以推出是那些指向被回收的对象的弱引用。

    /**
     * The default initial capacity -- MUST be a power of two.
     */
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    Entry<K,V>[] table;

    /**
     * The number of key-value mappings contained in this weak hash map.
     */
    private int size;

    /**
     * The next size value at which to resize (capacity * load factor).
     */
    private int threshold;

    /**
     * The load factor for the hash table.
     */
    private final float loadFactor;

    /**
     * Reference queue for cleared WeakEntries
     */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

    /**
     * The number of times this WeakHashMap has been structurally modified.
     * Structural modifications are those that change the number of
     * mappings in the map or otherwise modify its internal structure
     * (e.g., rehash).  This field is used to make iterators on
     * Collection-views of the map fail-fast.
     *
     * @see ConcurrentModificationException
     */
    int modCount;

这里主要要看一下Entry<K,V>[] table,注释中写的是The table, resized as necessary.明显是桶数组。其中的每个元素都是Entry<K,V>类型的,也就是说链表的节点就是Entry<K,V>类型的。

看一下Entry<K,V>的定义:

    /**
     * The entries in this hash table extend WeakReference, using its main ref
     * field as the key.
     */
    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;
        }

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

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

        public String toString() {
            return getKey() + "=" + getValue();
        }
    }

分析出几点:

  1. Entry<K,V> extends WeakReference说明Entry<K,V>本身是一个弱引用,那它的构造函数一定能返回一个弱引用,指向的是传给父类构造函数的第一个参数。同时绑定了一个引用队列。当指向的对象被回收后,弱引用就入队。
  2. Entry<K,V> 中包含一个属性Entry<K,V> next,next指向的是下一个Entry<K,V>对象,符合链表的定义。
  3. 第一次见到引用自身还能携带属性的,其中一个属性next又指向了同类的另一个引用,感觉有点绕。

偷来别人的图看一下:

img

WeakHashMap的构造函数

有两个参数,table的初始容量,加载因子,生成了table数组,元素类型是Entry<K,V>。

一般情况下用无参构造就行了。

    /**
     * Constructs a new, empty <tt>WeakHashMap</tt> with the given initial
     * capacity and the given load factor.
     *
     * @param  initialCapacity The initial capacity of the <tt>WeakHashMap</tt>
     * @param  loadFactor      The load factor of the <tt>WeakHashMap</tt>
     * @throws IllegalArgumentException if the initial capacity is negative,
     *         or if the load factor is nonpositive.
     */
    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)
            capacity <<= 1;
        table = newTable(capacity);
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
    }

    public WeakHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public WeakHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    private Entry<K,V>[] newTable(int n) {
        return (Entry<K,V>[]) new Entry<?,?>[n];
    }
WeakHashMap的一些工具方法
    private static final Object NULL_KEY = new Object();
    private static Object maskNull(Object key) {
        return (key == null) ? NULL_KEY : key;
    }
    static Object unmaskNull(Object key) {
        return (key == NULL_KEY) ? null : key;
    }


    private static boolean eq(Object x, Object y) {
        return x == y || x.equals(y);
    }

    /**理解为HashMap中的扰动函数就行
     * Retrieve object hash code and applies a supplemental hash function to the
     * result hash, which defends against poor quality hash functions.  This is
     * critical because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits.
     */
    final int hash(Object k) {
        int h = k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    /**返回table中的下标
     * Returns index for hash code h.
     */
    private static int indexFor(int h, int length) {
        return h & (length-1);
    }

关键方法expungeStaleEntries,作用是移除Entry<K,V>。

这些Entry<K,V>有一个共同的特征:

    /**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
        //取出已经入队的弱引用x (Entry<K,V> x)
        for (Object x; (x = queue.poll()) != null; ) {
            //一整个是一个同步块
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                //现在e是出队的那个弱引用,指向的是一个原本有强引用key指向的对象
                    Entry<K,V> e = (Entry<K,V>) x;
                //e里面的属性hash配合table的长度来确定e当前在哪个桶
                int i = indexFor(e.hash, table.length);

                //prev是桶的第一个元素
                Entry<K,V> prev = table[i];
                //经典的链表双指针遍历法
                Entry<K,V> p = prev;
                //p往前走,prev跟在后面,直到p==e为止
                while (p != null) {
                    Entry<K,V> next = p.next;
                    //找到了等于e的那个节点p
                    if (p == e) {
                        //这里说明e就是桶里的第一个元素
                        if (prev == e)
                            //直接置为next,e就从map中移除了
                            table[i] = next;
                        else
                            //不是第一个元素,跳过e
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        //到上面为止就已经完成了移除e
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

e这个东西是从引用队列里拿出来的,是一个弱引用,是对原本key指向的实际对象的一个弱引用。正是由于实际对象只剩这一个弱引用才会被GC回收。

然后e又是map中的一个元素,它本身是被强引用的,所以才有expungeStaleEntries()方法来从map中删除它的一个过程。

这里留了两个问题想不清楚:

  1. Must not null out e.next 是什么意思
  2. e.value = null; // Help GC 为什么要这样写
WeakHashMap的基操
PUT
    public V put(K key, V value) {
        //null也可以存进来
        Object k = maskNull(key);
        //计算key的hash
        int h = hash(k);
        //getTable()返回table,返回之前要expungeStaleEntries
        Entry<K,V>[] tab = getTable();
        //计算出table中的下标
        int i = indexFor(h, tab.length);

        //遍历链表,如果找到key一样的,并且值不相等的,给新值
        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];
        //插在了链表头,next指向原来的第一个元素
        tab[i] = new Entry<>(k, value, queue, h, e);
        //判断size是否达到扩容条件
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }

    private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return table;
    }
RESIZE
    void resize(int newCapacity) {
        //拿到老表
        Entry<K,V>[] oldTable = getTable();
        //老表长度看一下
        int oldCapacity = oldTable.length;
        //如果老表长度等于最大容量
        if (oldCapacity == MAXIMUM_CAPACITY) {
            //门槛设置为最大,这样put操作就不会总是进resize方法了
            threshold = Integer.MAX_VALUE;
            //扩不了了,结束
            return;
        }

        //来张新表
        Entry<K,V>[] newTable = newTable(newCapacity);
        //元素迁移
        transfer(oldTable, newTable);
        table = newTable;

        /*
         * If ignoring null elements and processing ref queue caused massive
         * shrinkage, then restore old table.  This should be rare, but avoids
         * unbounded expansion of garbage-filled tables.
         */
        //完成扩容之后,如果size大于门槛的1/2,门槛就按正常的设置为新的门槛
        //如果size不大于门槛的1/2,(size明显缩水了,可能是其他操作中的
        //expungeStaleEntries导致的,transfer里面也可能导致size缩水
        if (size >= threshold / 2) {
            threshold = (int)(newCapacity * loadFactor);
        } else {
            expungeStaleEntries();
            transfer(newTable, oldTable);
            table = oldTable;
        }
    }

TRANSFER
    private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
        //老表中的所有元素搬到新表
        for (int j = 0; j < src.length; ++j) {
            //j号桶的第一个元素
            Entry<K,V> e = src[j];
            //老表的j号桶直接清空
            src[j] = null;
            while (e != null) {
                //拿到e的下一个元素
                Entry<K,V> next = e.next;
                //对e做一些处理,e.get返回弱引用指向的实际对象,也就是key
                Object key = e.get();
                //如果key什么都没拿到
                if (key == null) {
                    //e不要了,把它的next和value都给null size--
                    e.next = null;  // Help GC
                    e.value = null; //  "   "
                    size--;
                } else {
                    //重新计算元素在数字中的下标
                    int i = indexFor(e.hash, dest.length);
                    //头插法
                    e.next = dest[i];
                    dest[i] = e;
                }
                //循环插入下一个节点
                e = next;
            }
        }
    }

这里又出现了Help GC的问题,不是很懂。

为什么这里可以写e.next = null了?而expungeStaleEntries方法里不写呢(Must not null out e.next说的是不是e.next不可以为null?)

containsValue
    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;
    }
get
    public V get(Object key) {
        //key为null也能查出来,NULL_KEY是一个 static final,计算出的hash总是一样
        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) {
            //看一下这里是怎么拿到key的,key不是e的属性,而是e弱引用的对象,直接get
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }
总结
  1. 关键是理解Entry<K,V>的结构,WeakHashMap的用法。
  2. 理清引用关系很重要。
  3. 留下了一个问题:expungeStaleEntries()里面e.value = null; // Help GC是如何help GC的?为什么不能e.next=null?(猜测可能和stale entries may be in use by a HashIterator这一句有关)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值