-------
细读源码是《马士兵教育》旗下赏析源码的栏目。
我们赏析源码的目的不是为了炫技,而是为了去理解作者的设计思想,并取其精华,去其糟粕,从而写出更加优秀的代码。
另一方面,也可以给面试加分。代码好坏的评价,不可避免地会代入个人的主观色彩,大家和而不同。
在《细读源码之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 : 11 : null1 : null1 : null1 : null1 : null1 : null0 : null0 : null0 : null0 : 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 nullnull org.example.sourcecode.WeakReference_Demo$WeakReferenceKey@404b9385null nullnull nullnull nullnull nullnull nullnull nullnull nullnull 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;elseprev.next = next;// Must not null out e.next;// stale entries may be in use by a HashIteratore.value = null; // Help GCsize--;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,点赞最高的前三名记得私信小姐姐哦~
你主动,我们就会有故事
👇👇👇





