众所周知,HashMap 是通过拉链法来解决哈希冲突的,也就是当哈希冲突时,会将相同哈希值的键值对通过链表的形式存放起来。

    JDK 7 时,采用的是头部插入的方式来存放链表的,也就是下一个冲突的键值对会放在上一个键值对的前面(同一位置上的新元素被放在链表的头部)。扩容的时候就有可能导致出现环形链表,造成死循环。

    resize 方法的源码:

    transfer 方法用来转移,将小数组的元素拷贝到新的数组中。

    注意 和 newTable[i] = e 这两行代码,就会将同一位置上的新元素被放在链表的头部。

    扩容前的样子假如是下面这样子。

    那么正常扩容后就是下面这样子。

    为什么 HashMap 是线程不安全的? - 图2

    假设现在有两个线程同时进行扩容,线程 A 在执行到 newTable[i] = e; 被挂起,此时线程 A 中:e=3、next=7、e.next=null

    线程 B 开始执行,并且完成了数据转移。

    此时,7 的 next 为 3,3 的 next 为 null。

    随后线程A获得CPU时间片继续执行 ,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

    为什么 HashMap 是线程不安全的? - 图5

    执行下一轮循环,此时 e=7,原本线程 A 中 7 的 next 为 5,但由于 table 是线程 A 和线程 B 共享的,而线程 B 顺利执行完后,7 的 next 变成了 3,那么此时线程 A 中,7 的 next 也为 3 了。

    采用头部插入的方式,变成了下面这样子:

    好像也没什么问题,此时 next = 3,e = 3。

    进行下一轮循环,但此时,由于线程 B 将 3 的 next 变为了 null,所以此轮循环应该是最后一轮了。

    接下来当执行完 e.next=newTable[i] 即 3.next=7 后,3 和 7 之间就相互链接了,执行完 newTable[i]=e 后,3 被头插法重新插入到链表中,执行结果如下图所示:

    为什么 HashMap 是线程不安全的? - 图7

    套娃开始,元素 5 也就成了弃婴,惨~~~

    不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序,参照的这一篇。

    但多线程同时执行 put 操作时,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。

    put 的源码:

    问题发生在步骤 ② 这里:

    两个线程都执行了 if 语句,假设线程 A 先执行了 ,那 table 是这样的:

    为什么 HashMap 是线程不安全的? - 图9

    接着,线程 B 执行了 tab[i] = newNode(hash, key, value, null),那 table 是这样的:

    3 被干掉了。

    线程 A 执行put时,因为元素个数超出阈值而出现扩容,线程B 此时执行get,有可能导致这个问题。

    注意来看 resize 源码:

    线程 A 执行完 table = newTab 之后,线程 B 中的 table 此时也发生了变化,此时去 get 的时候当然会 get 到 null 了,因为元素还没有转移。