暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

细读源码之WeakHashMap

马士兵 2021-06-09
327
↓推荐关注↓
昨天中奖的三个小伙伴记得私信小姐姐,今天也有趣题哦~

-------

细读源码是《马士兵教育》旗下赏析源码的栏目。

我们赏析源码的目的不是为了炫技,而是为了去理解作者的设计思想,并取其精华,去其糟粕,从而写出更加优秀的代码。

另一方面,也可以给面试加分。码好坏的评价,不可避免地会代入个人的主观色彩,大家和而不同。

在《细读源码之JAVA引用类型》一文中,介绍了JAVA平台下的四种引用类型,接下来介绍一下JDK中使用弱引用WeakReference的类WeakHashMap。

本文主要从以下三个方面进行讲解:

1.WeakHashMap介绍

2.WeakHashMap核心源码解析

3.WeakHashMap和HashMap的不同

4.WeakHashMap的应用


一.WeakHashMap介绍

WeakHashMap源码上的注释详细描述了WeakHashMap的特点:

An entry in a WeakHashMap will automatically be removed when its key is no longer in ordinary use. More precisely, the presence of a mapping for a given key will not prevent the key from being discarded by the garbage collector, that is, made finalizable, finalized, and then reclaimed. When a key has been discarded, its entry is effectively removed from the map, so this class behaves somewhat differently from other Map implementations.

简单翻译一下:WeakHashMap存储的entry,当key不再被使用(被GC回收)后,entry对象就会被自动删除。举例说明,代码如下:

    public static void main(String args[]) {
    WeakHashMap map = new WeakHashMap();
    String key = new String("1");
    map.put(key, "1");
    System.out.println(map.size() + " : " + map.get("1"));
    key = null;
    System.gc();
    for (int i = 0; i < 10; i++) {
    int size = map.size();
    System.out.println(size + " : " + map.get("1"));
    }
    }

    代码执行结果如下:

      1 : 1
      1 : null
      1 : null
      1 : null
      1 : null
      1 : null
      1 : null
      0 : null
      0 : null
      0 : null
      0 : null

      1.首先往WeakHashMap中put一个元素,执行System.out.println(map.size() + " : " + map.get("1")),打印出1 : 1;

      2.把key设置为null,调用System.gc触发gc,并没有执行过remove或者clear方法,WeakHashMap的size就由原来的1变成了0,get出来的对象变成了null,最后打印的结果是0 : null;

      3.至于打印的结果第二行是1 : null,后面又变成0 : null的原因是:GC执行垃圾回收和把关联的Reference放入ReferenceQueue中,两个动作不是同步进行的,有一定的时间差。还是举个例子说明,代码如下:

        public static void main(String[] args) {
        ReferenceQueue q = new ReferenceQueue();
        String key = new String("1");
        WeakReferenceKey weakReferenceKey = new WeakReferenceKey(key, q);
        key = null;
        System.gc();
        for (int i = 0; i < 10; i++) {
        key = weakReferenceKey.get();
        WeakReferenceKey itemInQueue = (WeakReferenceKey) q.poll();
        System.out.println(key + " " + itemInQueue);
        }
        }


        private static class WeakReferenceKey extends WeakReference<String> {
        public WeakReferenceKey(String referent, ReferenceQueue<? super String> q) {
        super(referent, q);
        }
        }

        代码行结果如下:

          null null
          null org.example.sourcecode.WeakReference_Demo$WeakReferenceKey@404b9385
          null null
          null null
          null null
          null null
          null null
          null null
          null null
          null null

          当循环执行第一次的时候,执行weakReferenceKey.get()的时候,GC已经执行垃圾回收,已经get不出数据了,但是ReferenceQueue中还没有put 回收key关联的weakReferenceKey对象,执行poll的时候,返回的数据是null。

          当循环执行到第二次的时候,才从ReferenceQueue中poll出数据。从侧面说明,GC执行垃圾回收和把关联的Reference放入ReferenceQueue中,并不是同步进行的,它们之前有一定的时间差。

          所以第一个示例代码出现1 : null这种奇怪不匹配数据的根本原因是:调用WeakHashMap的get方法的时候,会调用entry的get方法,也就是WeakReference的get方法,gc后已经获取不到数据了,得到了null,而此时ReferenceQueue中还没有数据,调用expungeStaleEntries方法(文章第二部分详细讲解),就暂时不能清除掉entry数据,所以size还是1。


          二.WeakHashMap源码解析

          1.Entry定义

          WeakHashMap的table定义为:Entry<K,V>[] table,而Entry类定义代码如下:

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

            Entry类的解析如下:

            1.Entry类继承了WeakReference弱引用,构造函数中调用super(key, queue)把key和弱引用的referent字段关联,而queue是WeakHashMap的一个字段,存储被GC回收key关联的Entry对象;

            2.Entry的hash字段,缓存的是key的hash,这样就不用每次使用的时候,都调用hashCode方法进行计算;更重要的是,如果不存储hash值,当key被回收后变成null以后,就无法计算key的hash值了,这个时候要删除这个entry元素,就只能遍历table中所有的数据了。

            3.Entry的next字段,存储的Hash冲突链表的下一个元素。


            2.expungeStaleEntries方法

            expungeStaleEntries方法的功能非常简单,就是删除table中那些key被GC回收,entry对象,代码如下:

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

              1.expungeStaleEntries方法执行的过程非常简单,就是从队列queue中不断的poll方法,获取那些key被GC回收的entry对象,然后从table中删除;

              2.expungeStaleEntries方法是一个被动调用过程,只有调用get,size等方法,触发了expungeStaleEntries方法执行后,才会做失效key关联entry的清除。如果WeakHashMap数据构造完成后,不再做任何操作,失效的entry都会存储在队列中,并且不会从table中删除。

              三.WeakHashMap和HashMap的不同点

              1.Map.Entry的key的引用类型不同

              WeakHashMap的Entry定义如下:

                private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
                }

                HashMap的Entry定义如下:

                  static class Entry<K, V> implements Map.Entry<K, V> {
                  }

                  WeakHashMap的Entry的key是弱引用对象,而HashMap的Entry的key是强引用对象,强引用的结果只要不对HashMap中的元素进行删除,key就永远不会被GC回收,哪怕这个key永远不能被访问。

                  2.size和get等方法的执行结果

                  WeakHashMap的size方法返回的大小,可能比add的元素的个数要少,因为有些entry被expungeStaleEntries方法清理掉。add的元素,再次get,有可能返回null,因为弱引用的key,在没有其他强引用指向时,会被GC回收。

                  而HashMap只要不进行remove或者clear操作,执行size和get方法就是稳定,不会因为GC而改变相关的数据。

                  3.Hash冲突的解决方式

                  WeakHashMap只使用链表去解决Hash冲突,而HashMap使用链表 + 红黑树去解决Hash冲突;

                  4.实现的接口不同

                  WeakHashMap没有实现Cloneable, Serializable接口,不能进行拷贝和序列化,而HashMap实现了Cloneable, Serializable接口,可以进行浅拷贝和序列化。

                  5.Entry<K,V>[] table初始化时机不同

                  WeakHashMap的table是在构造函数中进行初始化的,而HashMap的table在构造函数执行完成后,table还是null,只有在第一次put元素的时候,才进行初始化。


                  四.WeakHashMap的应用

                  tomcat包中的ConcurrentCache,使用了WeakHashMap,我们看看如何应用的:

                  1.添加tomcat的pom依赖

                    <dependency>
                    <groupId>org.apache.tomcat</groupId>
                    <artifactId>tomcat-catalina</artifactId>
                    <version>10.0.5</version>
                    </dependency>

                    2.ConcurrentCache源码如下:

                      public final class ConcurrentCache<K, V> {
                      private final int size;
                      private final Map<K, V> eden;
                      private final Map<K, V> longterm;


                      public ConcurrentCache(int size) {
                      this.size = size;
                      this.eden = new ConcurrentHashMap(size);
                      this.longterm = new WeakHashMap(size);
                      }


                      public V get(K k) {
                      V v = this.eden.get(k);
                      if (v == null) {
                      synchronized(this.longterm) {
                      v = this.longterm.get(k);
                      }


                      if (v != null) {
                      this.eden.put(k, v);
                      }
                      }


                      return v;
                      }


                      public void put(K k, V v) {
                      if (this.eden.size() >= this.size) {
                      synchronized(this.longterm) {
                      this.longterm.putAll(this.eden);
                      }


                      this.eden.clear();
                      }


                      this.eden.put(k, v);
                      }
                      }

                      源码分析:

                      A.put 方法

                      this.eden.size() >= this.size时,说明eden存储的数据比较多,需要借助longterm去清理部分数据。这个时候把eden的数据转移到longterm中,因为longterm是WeakHashMap类型,有自动清理被gc的key关联的entry对象功能。

                      最后把数据put到eden中。

                      B.get 方法

                      首先从eden中获取数据,如果获取到,直接返回;如果获取不到,再从longterm中获取,如果longterm中获取到了,根据程序的局部性原理,在把数据重新放入eden中,方便下次获取。

                      ConcurrentCache使用了ConcurrentHashMap和WeakHashMap的各自特性,实现了一个相对完善的缓存功能。

                      到此为此,WeakHashMap的内容就结束了,下一文章继续讲解弱引用的应用,敬请期待。

                      --------这--是--分--割--线--------

                      公布上期中奖名单


                      昨天发完感觉不太尽兴,要不然咱们今天继续答题?

                      ------ 每日一问 ------

                      一间囚房里关押着两个犯人。每天监狱都会为这间囚房提供一罐汤,让这两个犯人自己来分。

                      起初,这两个人经常会发生争执,因为他们总是有人认为对方的汤比自己的多。后来他们找到了一个两全其美的办法:一个人分汤,让另一个人先选。

                      可是,现在这间囚房里又进来一个新犯人,现在是三个人来分汤。必须寻找一个新的方法来维持他们之间的和平,该怎么办?

                      奖品:把你的解题思路评论区,点赞最高的前三凭截图私信小姐姐,领取实体书一本~

                      截止日期:2021/06/10 14:00,点赞最高的前三名记得私信小姐姐哦~

                      你主动,我们就会有故事

                      👇👇👇

                      文章转载自马士兵,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                      评论