jdk1.8的hashmap扩容时会发生数据丢失吗,大家来看看我的说法对不对
JDK1.8 HashMap 并发扩容数据丢失问题分析
一、核心结论
JDK 1.8 的 HashMap 仅通过尾插法解决了 JDK1.7 并发扩容时环形链表死循环问题,但没有解决并发扩容导致的数据丢失问题,HashMap 依旧线程不安全,多线程同时触发扩容依然会出现元素丢失、数据覆盖现象。
二、场景前置条件
HashMap 原始数组 table 容量 = 2
数组下标 1 位置链表:
3 -> 7 -> 5存在两个并发线程:线程 A、线程 B,同时执行扩容
resize()方法JDK1.8 扩容逻辑:按节点 hash 高位区分,原链表拆分为两条子链表,低位链表留在原下标,高位链表迁移至
原下标 + 原容量位置,采用尾插法迁移节点
三、线程调度时序复现数据丢失过程
步骤 1:两线程同时进入扩容逻辑,线程 A 中途阻塞
线程 A、B 同步读取旧数组,开始遍历下标 1 的链表迁移节点;
线程 A 遍历到节点e=3,预存下一个节点引用next=7,此时 CPU 切换,线程 A 被阻塞挂起。
步骤 2:线程 B 完整执行扩容,完成数组替换
线程 B 不受阻塞,完整遍历整条链表3->7->5,按 hash 拆分迁移:
元素 5:hash 高位为 0,留在新数组下标 1;
元素 3、7:hash 高位为 1,迁移至新数组下标 3,形成链表
3->7->null;线程 B 完成全部节点迁移,将扩容后的新数组赋值给 HashMap 全局变量
table
,旧数组引用失效。
此时内存中最新 table 状态:
table[1] = 5table[3] = 3 -> 7 -> null
步骤 3:线程 A 恢复时间片,基于旧引用继续遍历丢数据
线程 A 被唤醒后,持有的仍是扩容前旧链表的引用,继续执行循环:
先处理节点
e=3,完成迁移;读取预存的
next=7,赋值给e,继续处理节点 7;读取节点 7 的后继指针
7.next = null,next赋值为 null;循环判定
next == null,直接终止链表遍历。
步骤 4:最终结果 —— 元素 5 永久丢失
线程 A 只遍历了3、7两个节点,完全没有读取到节点 5,不会对 5 做任何迁移操作;
线程 A 迁移完成后覆盖数组,最终新数组中不存在元素 5,数据丢失。
