环境和版本

  • Mac M1
  • IDEA 2021
  • Zulu Open JDK

    Mark*

  • 有关于WeakHashMap的部分,会在后续JUC或者JVM部分进行再次对比讲解。

    简介

  • WeakHashMap,从名字可以看出它是某种 Map。它的特殊之处在于 WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法

  • 更直观的说,当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况

    调用两次size()方法返回不同的值; 两次调用isEmpty()方法,第一次返回false,第二次返回true; 两次调用containsKey()方法,第一次返回true,第二次返回false,尽管两次使用的是同一个key; 两次调用get()方法,第一次返回一个value,第二次返回null,尽管两次使用的是同一个对象。

  • WeakHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到

  • 要明白 WeakHashMap 的工作原理,还需要引入一个概念 : 弱引用(WeakReference)。我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的有效引用 并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收
  • WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢?将一对key, value放入到 WeakHashMap 里并不能避免该key值被GC回收,除非在 WeakHashMap 之外还有对该key的强引用

    构造函数

    ```java public WeakHashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0)

    1. throw new IllegalArgumentException("Illegal Initial Capacity: "+
    2. 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); }

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

<a name="k0Wm2"></a>
# Weak的关键点

- WeakHashMap的Entry继承自WeakReference
```java
// ReferenceQueue
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

// Hash表
Entry<K,V>[] table;

// Entry继承了WeakReference
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();
    }
}

put方法

private static final Object NULL_KEY = new Object();

public V put(K key, V value) {
    Object k = maskNull(key);
    // 计算hash
    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++;
    // 拿到 tab[i] 
    // WeakHashMap 没有树化,其是拉链法
    Entry<K,V> e = tab[i];
    tab[i] = new Entry<>(k, value, queue, h, e);
    // 扩容
    if (++size >= threshold)
        resize(tab.length * 2);
    return null;
}

private static Object maskNull(Object key) {
    return (key == null) ? NULL_KEY : key;
}

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
private Entry<K,V>[] getTable() {
    expungeStaleEntries();
    return table;
}

// 核心方法
// 对死对象进行清理
// 因此,这里的引用处理并不是自动的,其实是我们在调用某些方法的时候处理,所以我们认为它不是一种自动的,只是表面上看起来是这种处理
private void expungeStaleEntries() {
    // 
    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);
            // 取出下标 prev
            Entry<K,V> prev = table[i];
            // 赋值p为prev
            Entry<K,V> p = prev;
            // 只要 p != null
            while (p != null) {
                // 取出 p 的下一个
                Entry<K,V> next = p.next;
                // 如果 p的下一个 == e
                if (p == e) {
                    // 如果 prev == e
                    if (prev == e)
                        // table[i] 给 next
                        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;
            }
        }
    }
}
  • A:使用一个继承于WeakReference的entry对象表示每一个kv对,其中的原引用对象即我们在放入map中的key值
  • B:为保证效率以及尽可能的不使用key值,hash经过预先计算。这样在定位数据及重新get时不再需要使用原引用对象
  • C:由queue拿到的事件对象,即这里的entry值。通过entry定位到具体的桶位置,通过链表计算将entry的前后重新连接起来(即p.pre.next = p.next)
  • 在每个WeakHashMap都有个ReferenceQueue queue,在Entry初始化的时候也会将queue传给WeakReference,这样当某个可以key失去所有强应用之后,其key对应的WeakReference对象会被放到queue里,有了queue就知道需要清理哪些Entry里。这里也是整个WeakHashMap里唯一加了同步的地方。 除了上文说的到resize中调用了expungeStaleEntries(),size()中也调用了这个清理方法。另外 getTable()也调了,这就意味着几乎所有其他方法都间接调用了清理。

    get方法

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

    总结

  • 非线程安全:关键修改方法没有任何同步,多线程环境下会有数据不一致的情况,所以使用的时候需要注意。

  • 不能自定义ReferenceQueue:WeakHashMap构造方法中没法指定自定的ReferenceQueue,如果用户想用ReferenceQueue做一些额外的清理工作的话就行不通了。如果即想用WeakHashMap的功能,也想用ReferenceQueue,貌似得自己实现一套新的WeakHashMap了
  • 单纯作为Map没有HashMap好:HashMap在Jdk8做了好多优化,比如单链表在过长时会转化为红黑树,降低极端情况下的操作复杂度。但WeakHashMap没有相应的优化,有点像jdk8之前的HashMap版本。
  • 其内部的实现原理,其实和之前我们分析的其他源码类似,所以只要过一遍就知道是啥意思了,所以此处省略哪些部分。

    参考文章

  • ReferenceQueue的使用:https://www.cnblogs.com/dreamroute/p/5029899.html

  • WeakHashMap:https://www.pdai.tech/md/java/collection/java-map-WeakHashMap.html