HashSet理解为什么jdk1.7中的头插法会形成环和死循环?

Posted zhangjin1120

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashSet理解为什么jdk1.7中的头插法会形成环和死循环?相关的知识,希望对你有一定的参考价值。

HashSet理解(一)java集合
HashSet理解(二)怎么做到值不重复
HashSet理解(三)add方法(jdk1.7及以前)是如何插值的
HashSet理解(四)为什么jdk1.7中的头插法会形成环和死循环?

jdk1.7中,多线程环境下,扩容时,单链表可能会产生环,导致死循环。

扩容的过程:从addEntry()到transfer()

之前的文章都分析的是jdk1.6,jdk1.6和jdk1.7的addEntry()方法有区别,但区别不大。看看1.7的扩容:

    void addEntry(int hash, K key, V value, int bucketIndex) {
    //当size大于等于某一个阈值thresholdde时候且该桶并不是一个空桶;
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//将容量扩容为原来的2倍,也就是32
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);//扩容后的,该hash值对应的新的桶位置
        }

        createEntry(hash, key, value, bucketIndex);//在指定的桶位置上,创建一个新的Entry
    }

看看resize()里面干了啥?

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//新建一个新表
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
        transfer(newTable, rehash);//完成旧表到新表的转移
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }


    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //注意这里是table,不是newTable
        //从0到table.length-1,依次遍历旧数组table
        for (Entry<K,V> e : table) {
            //从头结点开始遍历旧单链表
            while(null != e) {
                Entry<K,V> next = e.next;//引用next
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //找到新数组的下标;
                //原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
                int i = indexFor(e.hash, newCapacity);
                //头插法
                //第一次插入时,newTable[i]是null
                //第二次插入时,newTable[i]是第一次插入的e
                e.next = newTable[i];
                newTable[i] = e;
                
                e = next; //while循环继续向下走
            }
        }
    }

单线程下,旧表中的数据,是如何转移到新表的?

把头插法的核心代码罗列出来,其他代码暂时去掉如下:

        while(null != e) {
            Entry<K,V> next = e.next; //t2线程,执行到这里挂起
            e.next = newTable[i]; //1
            newTable[i] = e;  //2
            e = next; 
        }

  • 画个a-b-null的单链表,作为旧表的table[1],那么第一轮循环,e就是a,next就是b 。如下图:

  • e.next=newTable[i];就是a指向新表的头结点,头结点现在还是null。a与b的连接断开了。如下图:

  • newTable[i]=e; 头结点直接等于a,a进入新表。结果如下图:

  • e=next;e变为b, 继续执行e.next=newTable[i];,就是b指向新表的头结点a。结果如下图:

  • 继续执行newTable[i]=e; e是b, 头结点直接等于b。这是就形成了b-a-null的新的单链表。如下图。单线程通过头插法,把旧表的数据转移到扩容后的新表,就是这个过程。

多线程情况下,单链表的环是怎样形成的?死循环又是怎么回事?

假设两个线程t1,t2同时扩容,同时转移数据。t1,t2都执行完下面的第1行代码,这时,t2挂起,t1线程继续走上面的转移数据流程。

        while(null != e) {
            Entry<K,V> next = e.next; //1   t2线程,执行到这里挂起
            e.next = newTable[i]; //2
            newTable[i] = e;  //3
            e = next; //4 
        }

 等到t1走完上述流程后,t2开始执行第2行代码,这时t2线程挂起前,保存的e还是a, next是b。执行e.next=newTable[i];就是令a指向b,这时a,b就形成了环。结果如下:

 程序还没有结束,e=next;next是b, 不为null, while循环继续执行。

e.next = newTable[i]; 
newTable[i] = e;    
e = next; 

 明显,a的next是b,b的next是a,所以e一直都不会为null,while循环永远不会退出,就出现了死循环。

感谢:
hashmap头插法和尾插法区别_一个跟面试官扯皮半个小时的HashMap
HashMap工作原理和扩容机制

以上是关于HashSet理解为什么jdk1.7中的头插法会形成环和死循环?的主要内容,如果未能解决你的问题,请参考以下文章

HashMap在1.7 1.8中的线程安全问题

HashSet理解怎么做到值不重复

HashSet理解java集合

单链表的头插法

链表的头插法和尾插法

单链表的头插法与尾插法代码实现及详解