必威体育Betway必威体育官网
当前位置:首页 > IT技术

WeakHashMap源码探讨(基于JDK1.8)

时间:2019-08-17 23:11:06来源:IT技术作者:seo实验室小编阅读:79次「手机版」
 

weakhashmap

一,基本概念

①简介

weakhashmap跟普通的HashMap不同,WeakHashMap的行为一定程度上基于垃圾收集器的行为,因此一些Map数据结构对应的常识在WeakHashMap上会失效——size()方法的返回值会随着程序的运行变小,isempty()方法的返回值会从false变成true等等。

②WeakHashMap的继承结构

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

③WeakHashMap中重要的成员变量

Entry<K,V>[] table;  //底层采用数组+单向链表
private int size;  //保存的节点数
private int threshold;  //size超过此阀值时,需要resize,扩容2倍
private final float loadFactor;  //负载因子
private final Referencequeue<Object> queue = new ReferenceQueue<>();  //保存的是“已被GC清除”的“弱引用的键”
int modCount;  //用来帮助实现fail-fast机制

WeakHashMap实际上是用Entry数组来存储数据的,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。

Entry<K,V>[] table;

Entry类的结构如下:

private static class Entry<K,V> extends weakreference<Object> implements Map.Entry<K,V> 

WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,然后被终止并回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。

这个“弱键”的原理呢?大致上就是,通过WeakReference和ReferenceQueue实现的。 

④关于java中的引用类型

引用类型主要分为4种:

①强引用(Strong Reference)就是永远不会回收掉被引用的对象,比如说我们代码中new出来的对象。

②软引用(SoftReference)表示有用但是非必需的,如果系统内存资源紧张,可能就会被回收。

③弱引用(WeakReference)表示非必需的对象,只能存活到下一次垃圾回收发生之前。

④虚引用(PhantomReference)是最弱的,这个引用无法操作对象。

WeakHashMap中用WeakReference,也就是虚引用来实现。

⑤关于引用队列ReferenceQueue 

引用队列相当于一个电话簿一样的东西,用于监听和管理在引用对象中被回收的对象。

下面帮助理解ReferenceQueue 的demo。

public class ReferenceTest {
    private static final int _1MB = 1024*1024;//设置大小为1MB

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();//用引用队列进行监控引用的回收情况
        Object value = new Object();
        Map<Object, Object> map = new HashMap<Object, Object>();
        for (int i = 0; i < 100; i++) {//循环100次把数据插入到弱应用中(WeakReference), 同时把弱引用作为key存入HashMap
            byte[] bytes = new byte[_1MB];
            //每个引用中都有关联引用队列(referenceQueue)的构造器,用引用队列监听回收情况
            //如此,那么每次WeakReference中的bytes被回收之后,那么这个weakReference对象就会放入引用队列
            WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);
            map.put(weakReference, value);
        }

        Thread thread = new Thread(new Runnable() {//线程通过调用引用队列的情况查看那些对象被回收
            @SuppressWarnings("unchecked")
            public void run() {
                try {
                    int cnt = 0;
                    WeakReference<byte[]> k;
                    while ((k = (WeakReference<byte[]>) referenceQueue.remove()) != null) {//返回被回收对象的引用(注意本例中被回收的是bytes)
                        System.out.println((cnt++)+"回收了"+k);
                        System.out.println("map的size = " + map.size());//用于监控map的存储数量有没有发生变化
                    }
                } catch (Exception e) {
                    // TODO: handle exception
                }
            }
        });

        thread.start();
    }

}

部分输出如下。

85回收了java.lang.ref.WeakReference@610455d6
map的size = 100
86回收了java.lang.ref.WeakReference@2503dbd3
map的size = 100
87回收了java.lang.ref.WeakReference@45ee12a7
map的size = 100
88回收了java.lang.ref.WeakReference@4554617c
map的size = 100
89回收了java.lang.ref.WeakReference@266474c2
map的size = 100
90回收了java.lang.ref.WeakReference@74a14482
map的size = 100

梳理一下流程。

1、bytes对象存入到weakReference对象中。 

2、weakReference对象作为key,一个Object作为值存入HashMap中。

3、GC回收了bytes对象,这个时候就要把引用这个对象的weakReference对象存储到ReferenceQueue中。

4、死循环ReferenceQueue, 打印出被回收的对象。

在这里,jvm回收了弱引用bytes,但没有回收作为map中的key的weakReference,因此每次我们打印的size都是100。

上面这个逻辑就是核心WeakHashMap的实现,WeakHashMap只不过比上述的代码多了一步:把引用回收的对象从Map中移除罢了。

二,WeakHashMap中ReferenceQueue如何帮助实现弱键?

ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:

1、新建WeakHashMap,将“键值对”添加到WeakHashMap中。

实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。

2、当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。

3、当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。

这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。

测试demo如下。

public class WeakTest {
    private static final int _1MB = 1024 * 1024;//设置大小为1MB

    public static void main(String[] args) throws InterruptedException {
        Object value = new Object();
        WeakHashMap<Object, Object> map = new WeakHashMap<Object, Object>();
        for (int i = 0; i < 100; i++) {//循环100次把数据插入WeakHashMap中
            byte[] bytes = new byte[_1MB];
            map.put(bytes, value);
        }
        while (true) {//死循环监控map大小变化
            Thread.sleep(500);//稍稍停顿,效果更直观
            System.out.println(map.size());//打印WeakHashMap的大小
            System.gc();//建议系统进行GC
        }
    }
}

执行结果。

9
0
0
0
.......

其实当我们在循环100次添加数据时,就已经开始回收弱引用了,因此我们会看到第一次打印的size是9,而不是100,在打印了大小之后,建议系统(System.gc())发起一次GC操作,为什么说是建议呢?因为系统不一定会接收到你指令就会发生GC的。一旦GC发生,那么弱引用就会被清除,导致WeakHashMap的大小为0。 同时,值得一提的是,存在WeakHashMap中的数据,并不会平白无故就给你移除了map中的数据,必然是你触发了一些操作,在上述代码中size方法就会触发这个操作。

public int size() {
        if (size == 0)
            return 0;
        expungeStaleEntries();
        return size;
    }

expungeStaleEntries方法代码如下。

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

这个方法,是从table中删除过时的条目,也就是弱引用被清除了之后,我们调用size方法来获取WeakHashMap的大小时,size方法会触发上面的方法,然后WeakHashMap去清除数据。

梳理上面代码的流程如下。

①:循环遍历引用队列(queue), 如果发现某个对象被GC了,那么就开始处理。 

②:如果被处理的这个节点是头节点,那么直接把该节点的下个节点放到头节点,然后帮助GC去除value的引用,接着把WeakHashMap的大小减1。 

③:如果被处理的这个节点不是头结点,那么就需要把这个节点的上个节点中的next指针直接指向当前节点的下个节点。意思就是a->b->c,这个时候要移除b,那么就变成a->c。然后帮助GC去除value的引用,接着把WeakHashMap的大小减1。

三,WeakHashMap的重要接口

①public V get(Object key) 方法

public V get(Object key) {
    Object k = maskNull(key);
    int h = hash(k);
    Entry<K,V>[] tab = getTable();
    //获取桶在table中的index
    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;
}

②public boolean containskey(Object key)

public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    /**
     * Returns the entry associated with the specified key in this map.
     * Returns null if the map contains no mAPPing for this key.
     */
    Entry<K,V> getEntry(Object key) {
        Object k = maskNull(key); //因为此map容器允许Key为null的,只是存放进去的null都做了NULL_KEY处理,所以要检查一遍是否key为null
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length); //获取桶在table的index
        Entry<K,V> e = tab[index]; //该桶的第一个Entry结点
        while (e != null && !(e.hash == h && eq(k, e.get()))) //循环找到对应的Key
            e = e.next;
        return e;
    }

③public V put(K key, V value)

/**
 * 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
 *
 * @param key 指定key
 * @param value 指定value
 * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
 */
public V put(K key, V value) {
    Object k = maskNull(key);
    int h = hash(k); //获取该key的哈希地址
    Entry<K,V>[] tab = getTable();
    int i = indexFor(h, tab.length); //找到该哈希地址所在的桶的index

    for (Entry<K,V> e = tab[i]; e != null; e = e.next) { 
        if (h == e.hash && eq(k, e.get())) {
            V oldValue = e.value;
            if (value != oldValue)
                e.value = value;
            return oldValue;
        }
    }

    modCount++;
    Entry<K,V> e = tab[i];
    tab[i] = new Entry<>(k, value, queue, h, e);
    if (++size >= threshold)
        resize(tab.length * 2);
    return null;
}

④public void putAll(Map<? extends K, ? extends V> m) 

public void putAll(Map<? extends K, ? extends V> m) {
        int numKeysToBeAdded = m.size();
        if (numKeysToBeAdded == 0)
            return;

        if (numKeysToBeAdded > threshold) { //如果要加入的map的原size大于阀值
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)
                newCapacity <<= 1; //可以看到,newCapacity总是以两倍的大小在扩大
            if (newCapacity > table.length)
                resize(newCapacity);
        }

        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }

⑤public V remove(Object key)

该方法本质是删除某个单链表,即同一个桶中的内容都要被清除。

public V remove(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (h == e.hash && eq(k, e.get())) {
                modCount++;
                size--;
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return e.value;
            }
            prev = e;
            e = next;
        }

        return null;
    }

四,WeakHashMap的遍历方式

public class WeakHashMapIteratorTest {

    public static void main(String[] args) {
        int val = 0;
        String key = null;
        integer value = null;
        Random r = new Random();
        WeakHashMap map = new WeakHashMap();

        for (int i=0; i<100; i++) {
            val = r.nextint(100);
            key = String.valueOf(val);
            value = r.nextInt(5);
            map.put(key, value);
        }
        System.out.println("添加元素完成...");

        // 通过entrySet()遍历WeakHashMap的key-value
        iteratorHashMapByEntryset(map) ;

        // 通过keySet()遍历WeakHashMap的key-value
        iteratorHashMapByKeyset(map) ;

        // 单单遍历WeakHashMap的value
        iteratorHashMapJustValues(map);
    }

    /*
     * 通过entry set遍历WeakHashMap
     */
    private static void iteratorHashMapByEntryset(WeakHashMap map) {
        if (map == null)
            return ;

        String key = null;
        Integer integ = null;

        Iterator iter = map.entrySet().iterator();

        while(iter.hasNext()) {
            Map.Entry entry = (Map.Entry)iter.next();

            key = (String)entry.getKey();
            integ = (Integer)entry.getValue();
        }
        System.out.println("iteratorHashMapByEntryset--->>>size: " + map.size());

    }

    /*
     * 通过keyset来遍历WeakHashMap
     */
    private static void iteratorHashMapByKeyset(WeakHashMap map) {
        if (map == null)
            return ;
        String key = null;
        Integer integ = null;

        Iterator iter = map.keySet().iterator();
        while (iter.hasNext()) {
            key = (String)iter.next();
            integ = (Integer)map.get(key);
        }

        System.out.println("iteratorHashMapByKeyset--->>>size: " + map.size());

    }


    /*
     * 遍历WeakHashMap的values
     */
    private static void iteratorHashMapJustValues(WeakHashMap map) {
        if (map == null)
            return ;

        Collection c = map.values();
        Iterator iter= c.iterator();

        System.out.println("iteratorHashMapJustValues--->>>size: " + map.size());
        while (iter.hasNext()) {
            iter.next();
        }

    }
}

相关阅读

支付宝推出新活动,Python脚本能让你赚的更多!(附源码)

写在前面 近期,马云大哥又在支付宝推出新活动了,不对,马云已经辞职了。不好意思哈,小编忘了。 但是呢,这个活动可是实实在在存在的哦~

基于哈夫曼树的编码和译码

1.实验要求: 利用哈夫曼编码:要传输一些数据(比如英文单词), 设计一个利用哈夫曼算法的编码系统, 为这些单词编码, 并在接收端进行

基于场景的设计方法,具体怎么做?

分享一遍去年给运营协会分享的场景设计方法,也是我一直来工作的方法。这套方法其实是阿里的一位叫 @六天 的大神总结出来的,记得当

安卓系统源码下载

第一步下载基础包 wget -c https://mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-latest.tar 第二步解压基础包 tar xvf a

驼峰命名法和数据库下划线问题和一个mybatis的源码解

参考:   https://segmentfault.com/a/1190000010240142 驼峰式命名开关,数据库列和字段名全一致。 开启 后不论多少下划线都

分享到:

栏目导航

推荐阅读

热门阅读