2022-10-08并发编程系列00
请注意,本文编写于 734 天前,最后修改于 734 天前,其中某些信息可能已经过时。

JDK 7 HashMap 并发死链

死循环问题发生在JDK1.7版本中,造成这个问题主要是由于HashMap自身的运行机制,加上并发操作,从而导致了死循环。 在JDK1.7中HashMap的底层数据实现是数组+链表的方式

初始状态 :

image-20221004201547959

JDK7 的 HashMap在数据添加时使用的是 头插入 ,如下图所示:

image-20221004201634050

正常情况下的扩容应该是下面的情况 :

image-20221004202000866

数组在扩容以后, 使用头插法将旧的元素复制到新的数组上.

死循环执行步骤1

死循环是因为并发HashMap扩容导致的,并发扩容的第一步,线程T1 和线程T2 要对HashMap进行扩容操作,此时T1和T2指向的是链表的头结点元素A,而T1和T2的下一个节点,也就是T1.next和T2.next指向的是B节点,如下图所示:

image-20221004202635490

死循环执行步骤2

死循环的第二步操作是,线程T2时间片用完进入休眠状态,而线程T1开始执行扩容操作,一直到线程T1扩容完成后,线程T2才被唤醒,扩容之后的场景如下图所示:

image-20221004203226783

从上图可知线程T1执行之后,因为是头插法,所以HashMap的顺序已经发生了改变,但线程T2对于发生的一切是不可知的,所以它的指向元素依然没变,如上图展示的那样,T2指向的是A元素,T2.next指向的节点是B元素

死循环执行步骤3

当线程T1执行完,而线程T2恢复执行时,死循环就建立了,如下图所示:

image-20221004203423849

因为T1执行完扩容之后B节点的下一个节点是A,而T2线程指向的首节点是A,第二个节点是B,这个顺序刚好和T1扩完容完之后的节点顺序是相反的。T1执行完之后的顺序是B到A,而T2的顺序是A到B,这样A节点和B节点就形成死循环了,这就是HashMap死循环导致的原因

死链解决方案

HashMap死循环的常用解决方案有以下3个:

  • 使用线程安全容器ConcurrentHashMap替代(推荐使用此方案)。
  • 使用线程安全容器Hashtable替代(性能低,不建议使用)。
  • 使用synchronized或Lock加锁HashMap之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)。

补充 : JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能 够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)

总结

HashMap死循环发生在JDK1.7版本中,形成死循环的原因是HashMap在JDK1.7使用的是头插法,头插法+链表+多线程并发+HashMap扩容,这几个点加在一起就形成了HashMap的死循环,解决死锁可以采用线程安全容器ConcurrentHashMap替代。

本文作者:兀坐晴窗独饮茶

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!