Java还要再学一遍基础(十一)WeakHashMap详解

WeakHashMap概述

WeakHashMap是以弱键实现的基于哈希表的存储映射数据的Map。当JVM对于这些弱键所指向的对象进行了清理回收之后,WeakHashMap会自动有效的将被回收了的映射从map中移除。

引用的相关知识

Java中的引用一共分为四种,分别为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference)和幻影引用(Phantom Reference)。
可以看到java.lang.ref包中有对应的几种:
这里写图片描述

  • 强引用(Strong Reference)
    强引用是java默认实现的引用。JVM会尽可能长时间的保留强引用的存在。当没有任何对象指向它时JVM将会回收。

    //这是一个强引用
    Object obj = new Object();
  • 弱引用(Weak Reference)
    弱引用是指当对象没有任何的强引用存在,在下一次的JVM的gc回收的时候它将会被回收。

    //这里是一个强引用
    Object obj = new Object();
    //new一个弱引用
    WeakReference<Object> weakRef = new WeakReference<Object>(obj);
    //obj = null后不再有强引用
    obj = null;
    //因为jvm的gc时间不确定所以循环
    while(true){
        //weakRef.get()当还未被回收的时候返回该对象,否则返回null
        if(weakRef.get() != null){
            System.out.println("is alive");
        }
        else{
            System.out.println("is not alive");
            break;
        }
    }

    运行结果:

    ...
    ...
    is alive
    is alive
    is alive
    is alive
    is alive
    is alive
    is not alive
  • 软引用(Soft Reference)
    软引用和弱引用基本性质一致,区别就是软引用JVM只会在虚拟机内存不足的时候才会去回收软引用,这使得软引用很适合做缓存应用。
  • 幻影引用(Phantom Reference)
    这种引用类型的get方法无论什么时候都会返回null。它唯一的作用就是可以用来记录对象是什么时候被gc回收的。

问题来了。怎么去记录对象的回收呢?
从上面的图片中包含的class可以看到其中有一个ReferenceQueue的class,翻译成中文就是引用队列,用来干什么?
原来java提供这个引用队列,可以把这个队列当作参数传到引用的构造器中,那么之后当对象被回收之后便会做一件事情,就是把被回收的对象放到这个因哟个队列中去。

利用ReferenceQueue记录对象被回收的例子:

Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<Object>(obj, queue);

System.out.println(obj);
obj = null;
System.gc();
while(true){
    //已经被回收
    if(weakRef.get() == null){
        Object o;
        //从队列中取
        if((o = queue.poll()) != null){
            System.out.println(o);
            break;
        }
    }
}

运行结果:

java.lang.Object@15db9742
java.lang.ref.WeakReference@6d06d69c

WeakHashMap

有了引用以及弱引用的相关概念并且了解HashMap的话WeakHashMap便很容易理解了。本文基于JDK1.8

1. 类定义

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

跟HashMap一样继承自AbstractMap并且实现Map接口,与之不同的是HashMap同时还实现了Cloneable, Serializable接口,这意味着WeakHashMap将不支持克隆和序列化。


2. 属性

//默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//节点数组
Entry<K,V>[] table;
//size
private int size;
//阈值
private int threshold;
//加载因子
private final float loadFactor;
//ReferenceQueue用于记录gc回收
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
//修改标志
int modCount;

可以看到基本上和HashMap没有什么太大的出入,明显的区别则是多了一个ReferenceQueue,很明显WeakHashMap就是通过这个引用队列来实现自动清理的。


2. 构造方法

public WeakHashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Initial Capacity: "+
                                           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);
}
//其他构造参数基本与HashMap相同,省略

这里有一点点与HashMap不同的是,HashMap在确定capacity的算法上有些不同。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;
    }

而WeakHashMap则是通过while循环实现。
同时HashMap的table数组并没有直接在构造器里面初始化,而是在加入元素之后延迟初始化的。

3. 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;
    }
   //......

这里看看构造器,传入了一个ReferenceQueue,似乎根最开始讲的ReferenceQueue记录gc回收很相似的用法。因为Entry继承了WeakReference类,所以现在的Entry对象管理的并不是强引用,而是弱引用也就是WeakReference。所以当被回收的时候自然就将会被这个引用队列所记录下来。之后只需要进行清理操作即可。


3. 关键的方法

  1. get方法。为了能让每次获取map中的元素的时候都能从已经自动清理过后的table数组中获取,所以get方法中加入了对应了清理操作。

    public V get(Object key) {
    Object k = maskNull(key);
    int h = hash(k);
    //这个getTable便是获取清理后的table
    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;
    }
    
    
    

    如何清理的:

    private Entry<K,V>[] getTable() {
        //expunge是抹去的意思  Stale:陈旧的
        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;
                //通过hash找到index
                int i = indexFor(e.hash, table.length);
    
                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                //遍历链表
                while (p != null) {
                    //记录下一个节点next
                    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;
                    }
                    //p记录下一个节点用作遍历,pref则是p的上一个节点
                    prev = p;
                    p = next;
                }
            }
        }
    }

    这下就很清楚了,原来是遍历队列中被回收的记录,再在table数组以及处理hash冲突的链表中去找到该记录并删除。
    初次之外,在resize和size方法调用的时候也使用到了上面的expungeStaleEntries方法。这里不做详细介绍。

  2. hash方法

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

和HashMap不相同,但是有点复杂….

WeakHashMap的特性

  • 存储键值对映射。
  • key和value可以为null
  • 线程不安全
  • 当键被gc回收能够自动清理map中的映射
  • 不支持克隆和序列化
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值