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

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

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

jdk1.7 put方法,没变

    public V put(K key, V value) {
    
    
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    
    
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    
    
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

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

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

	//jdk7中,仍然是先插入,再扩容
    void addEntry(int hash, K key, V value, int bucketIndex) {
    
    
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }
   //jdk7u60中,改为了先扩容再插入
    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
    }
	//头插
    void createEntry(int hash, K key, V value, int bucketIndex) {
    
    
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

threshold是一个临界值,等于16*0.75=12,size是所有entry的数量,并不是数组table[]的实际长度,在createEntry()中会执行size++;。所以HashMap扩容的两个条件:

  • entry的总数量大于等于阈值
  • 当前插入的entry,发生了hash碰撞

再说的直白一些,扩容前,数组table[]可能存在空元素,例如table[0]上有6个entry,table[1]上有5个entry,这时再向table[1]上面插入一个元素,就会扩容。但是此时,table[2],table[3]都是空的。

看看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工作原理和扩容机制

Guess you like

Origin blog.csdn.net/zhangjin1120/article/details/121070718