zoukankan      html  css  js  c++  java
  • JDK源码分析(9)之 WeakHashMap 相关

    平时我们使用最多的数据结构肯定是 HashMap,但是在使用的时候我们必须知道每个键值对的生命周期,并且手动清除它;但是如果我们不是很清楚它的生命周期,这时候就比较麻烦;通常有这样几种处理方式:

    • 由一个线程定时处理,可以是Timer或者ScheduledThreadPoolExecutor
    • 利用重写LinkedHashMap.removeEldestEntry(),实现 FIFOCache 或者 LRUCache;可以参考我之前写的一篇博客 LinkedHashMap 相关
    • 利用 WeakHashMap 的特性,如果逻辑比较复杂还可以直接使用Reference;这里可以参考 Reference 完全解读Reference 框架概览

    所以本文将主要介绍WeakHashMap的特性,以及补充一些关于 HashMap 实现的对比;相关 HashMap 的介绍也可以参考 HashMap 相关

    一、使用场景

    上面也介绍了,WeakHashMap适用于不是非常重要的缓存类似的场景;例如:

    WeakHashMap<Object, Integer> map = new WeakHashMap<>();
    
    for (int i = 0; i < 100; i++) {
      map.put(new Object(), i);
    }
    
    System.out.println(map.size());  // 1
    System.gc();                     // 2
    System.out.println(map.size());  // 3
    System.out.println(map.size());  // 4
    System.out.println(map.size());  // 5
    System.out.println(map);         // 6
    System.out.println(map.size());  // 7
    

    // 打印:
    100
    100
    100
    46
    {}
    0

    对于以上的结果你可能和我打印的不一样,WeakHashMap按照语义应该是,当 key 没有强引用指向的时候,会自动清除 key 和 value;我这里先解释它的释放过程,如果你觉得很清晰,那WeakHashMap你就算是掌握了;

    • 首先 for 循环结束的时候,key 已经没用强引用指向了,此时所有的 key 都是弱引用了;
    • 接下来执行1,因为我这里只有一个方法,新生代还有足够的空间,所以不会触发 GC,所以所有的 key 任然在堆里面,所以打印100;
    • 然后手动触发 GC,虽然System.gc();不一定会立即执行,但是我这里只有一个方法,所以肯定会执行 GC,这里可以打开 GC 日志查看,-verbose:gc;因为 所有的 key 都是弱引用,所以referent被致为 null,同时将 key 注册到 ReferenceQueue中;
    • 在执行 3-7 的时候,按语义 map 应该为空;但是将 key 注册到 ReferenceQueue并非原子性一次完成的,所以这里会打印不同的值,每注册完成一个,在 map 进行操作的时候,就会将其移除;

    将上面的代码改成多线程分析思路也是一样的,如果你觉得有不清楚的地方可以查看下文;

    二、WeakHashMap 源码分析

    1. 类定义

    public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
    

    可以看到虽然WeakHashMap也是基于哈希表,但是却并非像LinkedHashMap一样是继承于HashMap,并且WeakHashMap也没有实现Cloneable, Serializable两个接口,这是因为WeakHashMap基于WeakReference实现的,弱引用并不建议实现序列化,同时弱引用一般用于不是很重要的缓存,也就没必要实现Cloneable, Serializable两个接口了;

    2. 核心方法

    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
    
    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;
      }
    
      public K getKey() { }
      public V getValue() {
      public V setValue(V newValue) {
      public int hashCode() {
      public String toString() {
    }
    
    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);
    
          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;
          }
        }
      }
    }
    

    上面代码所列的ReferenceQueue,Entry,expungeStaleEntries()就是WeakHashMap实现的核心了;这里强烈建议要先看 Reference 完全解读Reference 框架概览 这两篇博客,里面同样的内容我也不会再赘述了;

    • Entry<K,V> extends WeakReference<Object>, 表明所有的节点都是WeakReference,而 key 则是 referent;
    • queue,所有 key 使用同一个ReferenceQueue监听器,每当 key 被回收的时候,entry 将会被注册到ReferenceQueue中;
    • expungeStaleEntries,将注册到ReferenceQueue中的 entry 移除,并将 value 置为 null;WeakHashMap的所有操作都先执行expungeStaleEntries,这样WeakHashMap就实现了自动回收不在需要的 key 和 value;

    三、性能对比

    其实上面的内容就已经将WeakHashMap的主要实现讲完了,但是我之前在看HashMap源码的时候,并没有对比 JDK1.7 和 JDK1.8,但是在这里发现其实WeakHashMap的实现和 JDK1.7 差不多,所以接下来我将简单对比一下WeakHashMapHashMap

    1. 容量计算

    WeakHashMapHashMap中都要求容量是2的幂,因为当容量为2的幂时,使用除留余数法计算哈希桶位置时可以使用hash % length = hash & (length-1)的性质进行优化;

    // WeakHashMap
    int capacity = 1;
    while (capacity < initialCapacity)
      capacity <<= 1;
    
    // HashMap
    static final int tableSizeFor(int cap) {
      int n = cap - 1;
      n |= n >>> 1;
      n |= n >>> 2;
      n |= n >>> 4;
      n |= n >>> 8;
      n |= n >>> 16;
      return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    

    简单测试可以得到:

    initCap = 10 50 100
    WeakHashMap 30 32 26
    HashMap 3 3 3

    代码比较简单我就不贴了,从上表也可以看到了tableSizeFor不仅高效而且稳定;

    2. 哈希计算

    // WeakHashMap
    final int hash(Object k) {
      int h = k.hashCode();
      h ^= (h >>> 20) ^ (h >>> 12);
      return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
    // HashMap
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    两种hash算法都是要避免极端的hashCode(),但是HashMap却更为透彻,因为影响哈希桶位置的只有 hash 的低位(容量2的n次方,n个低位),直接将高位与上低位,使高位 hash 参与位置计算,简洁且高效;

    此外还有put方法,但是里面还牵涉红黑树,对于本文就扯得有点远了,所以暂不讲;

    总结

    • WeakHashMapWeakReference的典型应用,在灵活应用WeakHashMap之后,如果有更为复杂的逻辑,可以直接使用Reference实现;
    • 另外WeakHashMap的自动回收机制是操作时检查,所以WeakHashMap里面即使有可回收对象,但是很久都没有操作也是没法及时清理,所以在使用的时候,需要经常对它操作一下,才能及时回收垃圾;
  • 相关阅读:
    JPA + EclipseLink + SAP云平台 = 运行在云端的数据库应用
    Java实现 LeetCode 500 键盘行
    Java实现 LeetCode 500 键盘行
    Java实现 LeetCode 498 对角线遍历
    Java实现 LeetCode 498 对角线遍历
    Java实现 LeetCode 498 对角线遍历
    Java实现 LeetCode 496 下一个更大元素 I
    Java实现 LeetCode 496 下一个更大元素 I
    Java实现 LeetCode 496 下一个更大元素 I
    Java实现 LeetCode 495 提莫攻击
  • 原文地址:https://www.cnblogs.com/sanzao/p/10367123.html
Copyright © 2011-2022 走看看