HashMap为什么线程不安全(死循环+数据丢失过程分析)

jdk1.7中的HashMap

扩容部分源码

扩容分为两步
1.创建一个数组容量为原来2倍的HashMap
2.遍历旧的Entry数组,把所有的Entry重新Hash到新数组

在开始之前我们先看一下扩容部分的源码

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;     
    // 遍历旧数组中的每个桶
    for (Entry<K,V> e : table) {      
        // 遍历桶中的元素Entry(是一个链表)
        while(null != e) {
            Entry<K,V> next = e.next;
            //如果是重新Hash,则需要重新计算hash值  
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //计算该Entry在新table数组中对应桶的下标Index
            /*
             * indexFor中的代码是return h & (length-1);  
             *可见hash对应的数组下标index是与hashmap的大小有关的,所以扩容不能直接把旧数据复制过来,需要遍历重新计算下标index的值
            */
            int i = indexFor(e.hash, newCapacity);         
            //头插法
            e.next = newTable[i];
            newTable[i] = e;
            //继续下一个元素(next是再上面就存下来了的,是原来链表的下一个元素)
            e = next;
        }
    }
}

扩容导致死循环

假设的前提条件:

  1. index求解为简单的用key mod 数组的长度
  2. 最开始size=2 , key=3,7,5,则由假设1可知3 7 5 都在table[1]中
  3. 然后进行resize,使size变为4

未resize前的数据结构:

单线程

如果在单线程下根据上方transfer的源码理解(遍历+头插法),最后扩容结果如下:

多线程

假设有两个线程A,B都在进行扩容操作,但是线程A遍历的时候在*处被暂时挂起了

 1    void transfer(Entry[] newTable, boolean rehash) {
 2         int newCapacity = newTable.length;
 3         for (Entry<K,V> e : table) {
 4             while(null != e) {
 5                 Entry<K,V> next = e.next;
 6                 if (rehash) {
 7                     e.hash = null == e.key ? 0 : hash(e.key);
 8                 }
 9                 int i = indexFor(e.hash, newCapacity);
10                 e.next = newTable[i];
11                 newTable[i] = e;--------* A被挂起
12                 e = next;
13             }
14         }
15     }

①此时A线程e=3 | next=7(上方代码第5行得)| i=3 (由假设1可知3%4=3) | e.next=null(10行代码 3.next=null)| 在执行第11行的时候被线程挂起

此时A线程的结果

A线程被挂起,B线程正常执行,完全执行完resize扩容操作后,结果如下:


★由于B执行完了,所以现在java内存中newtable和table中的Entry都是B线程执行完的最新值★
★即: newTable[1]=5 | 5.next=null | newTable[3]=7 | 7.next=3 | 3.next=null★

此时再切换回A线程,A线程重新继续执行11行的代码,①已经求得的临时变量的值是没变的。
上面①中的临时变量继续使用

11  newTable[i] = e;  -----(i=3,e=3带入得newTable[3]=3)
12  e = next;         -----(next=7,带入得e=7)

执行结果如下:

继续循环(此时e=7 由上次循环的代码可知):

5 next = e.next;                     -----(由上图可知7.next=3故得next=3)
9 i = indexFor(e.hash,newCapacity);  -----(由假设1可知index=7%4=3)
10 e.next = newTable[i];             -----(由上图可知newTable[3]=3故得e.next=3)
11 newTable[i] = e;                  -----(newTable[3]=7)
12 e = next                          -----(e=3)

结果如下:

继续循环(此时e=3 由上次循环的代码可知):

5 next = e.next;                     -----(由上图可知3.next=null故得next=null)
9 i = indexFor(e.hash,newCapacity);  -----(由假设1可知index=3%4=3)
10 e.next = newTable[i];             -----(由上图可知newTable[3]=7故得e.next=7)
11 newTable[i] = e;                  -----(newTable[3]=3)
12 e = next                          -----(e=null)

至此e=null,循环结束
结果如下:

可见这出现了一个循环链表,在之后只要涉及轮询hashmap数据结构,就会在这里发生死循环。

扩容导致数据丢失

假设前提条件

  1. index求解为简单的用key mod 数组的长度
  2. 最开始size=2 , key=7,5,3,则由假设1可知7 5 3 都在table[1]中
  3. 然后进行resize,使size变为4

    同样两个线程A,B都在进行扩容,A在*处被挂起
 1    void transfer(Entry[] newTable, boolean rehash) {
 2         int newCapacity = newTable.length;
 3         for (Entry<K,V> e : table) {
 4             while(null != e) {
 5                 Entry<K,V> next = e.next;
 6                 if (rehash) {
 7                     e.hash = null == e.key ? 0 : hash(e.key);
 8                 }
 9                 int i = indexFor(e.hash, newCapacity);
10                 e.next = newTable[i];
11                 newTable[i] = e;--------* A被挂起
12                 e = next;
13             }
14         }
15     }

①此时A线程e=7 | next=5(上方代码第5行得)| i=3 (由假设1可知7%4=3) | e.next=null(10行代码 3.next=null)| 在执行第11行的时候被线程挂起
执行结果如下:

A线程被挂起,B线程正常执行,完全执行完resize扩容操作后,结果如下:


★由于B执行完了,所以现在java内存中newtable和table中的Entry都是B线程执行完的最新值★
★即: newTable[1]=5 | 5.next=null | newTable[3]=3 | 3.next=7 | 7.next=null★
此时再切换回A线程,A线程重新继续执行11行的代码,①已经求得的临时变量的值是没变的。
上面①中的临时变量继续使用:①此时A线程e=7 | next=5(上方代码第5行得)| i=3 (由假设1可知3%4=3) | e.next=null(10行代码 7.next=null)| 在执行第11行的时候被线程挂起

11  newTable[i] = e;  -----(i=3,e=7带入得newTable[3]=7)
12  e = next;         -----(next=5,带入得e=5)

执行结果如下:

继续循环(此时e=5 由上次循环的代码可知):

5 next = e.next;                     -----(由上图可知5.next=null故得next=null)
9 i = indexFor(e.hash,newCapacity);  -----(由假设1可知index=5%4=1)
10 e.next = newTable[i];             -----(由上图可知newTable[1]=5故得e.next=5)
11 newTable[i] = e;                  -----(newTable[1]=5)
12 e = next                          -----(e=null)

至此e=null,循环结束
结果如下:
3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。

jdk1.8中的HashMap

jdk1.8中对HashMap进行了优化,如果发生hash碰撞,不采用头插法,改成了尾插法,因此不会出现循环链表的情况。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果插入的位置为null,没有hash碰撞,则直接插入元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        .........

但是在多线程的环境下仍然不安全,当两个线程A,B同时进行Put的操作的时候,都判断当前没有hash碰撞,然后同时进行直接插入,那么后面哪个线程的值Entry会把前一个线程插入的Entry给**覆盖**掉,发生线程不安全

原文地址:https://www.cnblogs.com/bendandedaima/p/13259284.html