JAVA基础(四)HashMap线程不安全问题分析

resize()死循环

在JDK1.8之前,HashMap在动态扩容时复制旧table中的链表结点到新扩容后的newTab中使用的是头插法,每个节点都是插入在链表的头部,这也是导致多线程环境下出现环形链表死循环的根本原因。

过程分析

清晰思路只简化出核心代码:

while(null != e) {
	//①断开链表前读取下一个节点,否则将会丢失链表
    Entry<K,V> next = e.next;
    //头插法,
    e.next = newTable[i];
    //复制节点到新数组
    newTable[i] = e;
    e = next;
}

假设目前HashMap的table的长度为2,扩容阈值为1。
JAVA基础(四)HashMap线程不安全问题分析

  1. 线程T1向HashMap中插入k3,当执行到代码①处时cpu执行时间耗尽,暂时被挂起。
    此时:
    原table中已经插入k3,并触发扩容,创建了新数组newTab,但是还没开始复制操作。核心代码中的e指针指向k3,next则指向k4
    JAVA基础(四)HashMap线程不安全问题分析

  2. 此时线程T2开始运行,T2向HashMap中插入k2,并完成了整个扩容操作。
    JAVA基础(四)HashMap线程不安全问题分析

  3. T1获得CPU时间继续运行,此时e–>k3,next–>k4。按照代码顺序运行,可以得出循环中每次复制的节点队列:
    JAVA基础(四)HashMap线程不安全问题分析JAVA基础(四)HashMap线程不安全问题分析JAVA基础(四)HashMap线程不安全问题分析
    JAVA基础(四)HashMap线程不安全问题分析
    此时k3–>k4,k4–>k3,形成了环形链表,在读取时会出现死循环。

原因分析

线程T2扩容复制进新的数组时使用的是头插法,所以导致了链表插入的倒序