Hashmap头插法死循环

  先来看一看老版本HashMap扩容代码:

void resize(int newCapacity)
{
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    //创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];
    //将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

   其中,重点在于transfer():

void transfer(Entry[] newTable)
{
  //复制一个原数组src,Entry是一个静态内部类,有K,V,next三个成员变量 Entry[] src
= table;
  //数组新容量
int newCapacity = newTable.length;// 从OldTable里摘一个元素出来,然后放到NewTable中 for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j];//取出原数组一个元素 if (e != null) {//判断原数组该位置有元素 src[j] = null;//原数组位置置为空 do {//对原数组某一位置下的一串元素进行操作 Entry<K,V> next = e.next;//next是当前元素下一个 int i = indexFor(e.hash, newCapacity);//i是元素在新数组的位置 e.next = newTable[i];//此处体现了头插法,当前元素的下一个是新数组的头元素 newTable[i] = e;//将原数组元素加入新数组 e = next;//遍历到原数组某一位置下的一串元素的下一个
      } while (e != null);
    }
  }
}

   接下来图示单线程情况下,do循环内的情况:

  初始:当前数组容量为2,有三个元素3、7、5,此处的hash算法是简化处理(对容量取模)。因此,3、7、5都在数组索引1对应的链表上。

  扩容新容量为2*2=4。

  第一步:当前Entry e对应3,next对应7,新位置i为3,然后将3插入新数组对应位置。

  第二步:当前Entry e对应7,next对应5,新位置i为3,然后将新数组对应索引处的元素3添加到7的尾巴后(头插),然后将7插入新数组对应位置。

  第三步:当前Entry e对应5,next对应null,新位置i为1, 然后将5插入新数组对应位置。

   接下来图示多线程情况下死循环场景:初始条件相同。

  如果有两个线程:

    线程一执行到 Entry<K,V> next = e.next; 便挂起了,即此时Entry e是3,next是7,3是在7前面的。

    线程二执行完成。

  此时如下图所示,线程一的3的next是7,而线程二的7的next是3。(此处是Entry里的next成员变量,在多个线程中相同Entry不冲突)。此时可以看出出现了死循环问题。

   如果此时线程一继续往下执行:

   第一步:当前Entry e对应3,next对应7,新位置i为3,然后将3插入新数组对应位置。

   第二步:当前Entry e对应7,next对应3(单线程情况下是5),新位置i为3,然后将7插入新数组对应位置。

   第三步:当前Entry e对应3,next对应7,此处死循环,永远不会跳出while循环。

 

总结归纳:多线程情况下,使用头插法会导致链表节点之间的关系混乱,出现倒排现象,例如原本3->7->5变成7->3,其他线程此时再进行扩容是会出现死循环。 

 单线程

0   

1        3 ->7 ->5

          e     next

     e next

       e  next=null   

0

1    5

2

3     7  -> 3    

0   

1        3 ->7 ->5

          e     next  线程池一中断

线程二执行完

0

1    5

2

3     7  -> 3    

线程一继续

    出现死循环问题

原文地址:https://www.cnblogs.com/qmillet/p/13054208.html