作为一个java开发者肯定都知道且使用HashMap,但估计大部分人都不太知道WeakHashMap。从类定义上来看,它和普通的HashMap一样,继承了AbstractMap类和实现了Map接口,也就是说它有着与HashMap差不多的功能。那么既然jdk已经提供了HashMap,为什么还要再提供一个WeakHashMap呢? 黑格尔曾经说过,存在必合理,接下来我们来看下为什么有WeakHashMap。
  先来想象一下你因为某种需求需要一个Cache,你肯定会面临一个问题,就是所有数据不可能都放到Cache里,或者放到Cache里性价比太低了。这个时候你可能很快就想到了各种Cache数据过期策略,目前也有一些优秀的包提供了功能丰富的Cache,比如Google的Guava Cache,它支持数据定期过期、LRU、LFU等策略,但它任然有可能会导致有用的数据被淘汰,没用的数据迟迟不淘汰(如果策略使用得当的情况下这都是小概率事件)。
  如果我现在说有种机制,可以让你Cache里不用的key数据自动清理掉,用的还留着,没有误杀也没有漏杀你信不信!没错WeakHashMap就是能实现这种功能的东西,这也是它和普通的HashMap不同的地方——它有自清理的机制。
  如果让你实现一种自清理的HashMap,你怎么做? 我的做法肯定是想办法先知道某个Key肯定没有在用了,然后清理到HashMap中对应的K-V。在JVM里一个对象没用了是指没有任何其他有用对象直接或者间接执行它,具体点就是在GC过程中它是GCRoots不可达的。 Jvm提供了一种机制能让我们感知到一个对象是否已经变成了垃圾对象,这就是WeakReference,不了解WeakReference的可以看下我上一篇介绍博客Java弱引用(WeakReferences)
  某个WeakReference对象所指向的对象如果被判定为垃圾对象,Jvm会将该WeakReference对象放到一个ReferenceQueue里,我们只要看下这个Queue里的内容就知道某个对象还有没有用了。 WeakHashMap就是这么做的,所以这里的Weak是指WeakReference。接下来让我们看下它的代码,看它具体是怎么实现的。
  

  1. private static final int DEFAULT_INITIAL_CAPACITY = 16;
  2. private static final int MAXIMUM_CAPACITY = 1 << 30;
  3. private static final float DEFAULT_LOAD_FACTOR = 0.75f;

  和普通HashMap一样,WeakHashMap也有一些默认值,比如默认容量是16,最大容量2^30,使用超过75%它就会自动扩容。

  1. private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
  2. V value;
  3. final int hash;
  4. Entry<K,V> next;
  5. Entry(Object key, V value,
  6. ReferenceQueue<Object> queue,
  7. int hash, Entry<K,V> next) {
  8. super(key, queue);
  9. this.value = value;
  10. this.hash = hash;
  11. this.next = next;
  12. }
  13. /*
  14. * 其他代码
  15. */
  16. }

  它的Entry和普通HashMap的Entry最大的不同是它继承了WeakReference,然后把Key做成了弱引用(注意只有Key没有Value),然后传入了一个ReferenceQueue,这就让它能在某个key失去所有强引用的时候感知到。
  

  1. public V put(K key, V value) {
  2. Object k = maskNull(key);
  3. int h = hash(k);
  4. Entry<K,V>[] tab = getTable();
  5. int i = indexFor(h, tab.length);
  6. for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
  7. if (h == e.hash && eq(k, e.get())) {
  8. V oldValue = e.value;
  9. if (value != oldValue)
  10. e.value = value;
  11. return oldValue;
  12. }
  13. }
  14. modCount++;
  15. Entry<K,V> e = tab[i];
  16. tab[i] = new Entry<>(k, value, queue, h, e);
  17. if (++size >= threshold)
  18. resize(tab.length * 2);
  19. return null;
  20. }

  put方法也很简单,用key的hashcode在tab中定位,然后判断是否是已经存在的key,已经存在就替换旧值,否则就新建Entry,遇到多个不同的key有同样的hashCode就采用开链的方式解决hash冲突。注意这里和HashMap不太一样的地方,HashMap会在链表太长的时候对链表做树化,把单链表转换为红黑树,防止极端情况下hashcode冲突导致的性能问题,但在WeakHashMap中没有树化。
  同样,在size大于阈值的时候,WeakHashMap也对做resize的操作,也就是把tab扩大一倍。WeakHashMap中的resize比HashMap中的resize要简单好懂些,但没HashMap中的resize优雅。WeakHashMap中resize有另外一个额外的操作,就是expungeStaleEntries(),就是对tab中的死对象做清理,稍后会详细介绍。

  1. public V get(Object key) {
  2. Object k = maskNull(key);
  3. int h = hash(k);
  4. Entry<K,V>[] tab = getTable();
  5. int index = indexFor(h, tab.length);
  6. Entry<K,V> e = tab[index];
  7. while (e != null) {
  8. if (e.hash == h && eq(k, e.get()))
  9. return e.value;
  10. e = e.next;
  11. }
  12. return null;
  13. }

  get方法就没什么特别的了,因为Entry里存了hash值和key的值,所以只要用indexFor定位到tab中的位置,然后遍历一下单链表就知道了。

  1. private void expungeStaleEntries() {
  2. for (Object x; (x = queue.poll()) != null; ) {
  3. synchronized (queue) {
  4. @SuppressWarnings("unchecked")
  5. Entry<K,V> e = (Entry<K,V>) x;
  6. int i = indexFor(e.hash, table.length);
  7. Entry<K,V> prev = table[i];
  8. Entry<K,V> p = prev;
  9. while (p != null) {
  10. Entry<K,V> next = p.next;
  11. if (p == e) {
  12. if (prev == e)
  13. table[i] = next;
  14. else
  15. prev.next = next;
  16. // Must not null out e.next;
  17. // stale entries may be in use by a HashIterator
  18. e.value = null; // Help GC
  19. size--;
  20. break;
  21. }
  22. prev = p;
  23. p = next;
  24. }
  25. }
  26. }
  27. }

  expungeStaleEntries就是WeakHashMap的核心了,它承担着Map中死对象的清理工作。原理就是依赖WeakReference和ReferenceQueue的特性。在每个WeakHashMap都有个ReferenceQueue queue,在Entry初始化的时候也会将queue传给WeakReference,这样当某个可以key失去所有强应用之后,其key对应的WeakReference对象会被放到queue里,有了queue就知道需要清理哪些Entry里。这里也是整个WeakHashMap里唯一加了同步的地方。
  除了上文说的到resize中调用了expungeStaleEntries(),size()中也调用了这个清理方法。另外 getTable()也调了,这就意味着几乎所有其他方法都间接调用了清理。

  除了上述几个和HashMap不太一样的地方外,WeakHashMap也提供了其他HashMap所有的方法,比如像remove、clean、putAll、entrySet…… 功能上几乎可以完全替代HashMap,但WeakHashMap也有一些自己的缺陷。

  关键修改方法没有提供任何同步,多线程环境下肯定会导致数据不一致的情况,所以使用时需要多注意。

  HashMap在Jdk8做了好多优化,比如单链表在过长时会转化为红黑树,降低极端情况下的操作复杂度。但WeakHashMap没有相应的优化,有点像jdk8之前的HashMap版本。
  

  WeakHashMap构造方法中没法指定自定的ReferenceQueue,如果用户想用ReferenceQueue做一些额外的清理工作的话就行不通了。如果即想用WeakHashMap的功能,也想用ReferenceQueue,貌似得自己实现一套新的WeakHashMap了。

  这里列举几个我所知道的WeakHashMap的使用场景。

  在阿里开源的Java诊断工具中使用了WeakHashMap做类-字节码的缓存。

  1. // 类-字节码缓存
  2. private final static Map<Class<?>/*Class*/, byte[]/*bytes of Class*/> classBytesCache
  3. = new WeakHashMap<Class<?>, byte[]>();

  WeakHashMap这种自清理的机制,非常适合做缓存了。

  ThreadLocal中用ThreadLocalMap存储Thread对象,虽然ThreadLocalMap和WeakHashMap不是一个东西,但ThreadLocalMap也利用到了WeakReference的特性,功能用途很类似,所以我很好奇为什么ThreadLocalMap不直接用WeakHashMap呢!
版权声明:本文为博主原创文章,转载请注明出处。 博客地址:https://xindoo.blog.csdn.net/

版权声明:本文为xindoo原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/xindoo/p/11421734.html