根本原因是resize操作非原子且无同步保护,多线程并发迁移链表时,线程a挂起后恢复执行会基于已被线程b修改的旧链表状态继续操作,导致节点跳过或重复插入而丢失;jdk7头插法还易引发环形链表,jdk8虽改尾插法避免成环,但迁移仍非原子,size计数不准还会误导扩容时机,加剧丢失风险。

HashMap 在高并发下调整大小(resize)时发生数据丢失,根本原因在于 resize 操作不是原子的,且多个线程共享同一张哈希表、无同步保护。整个过程涉及数组重建、节点遍历、重新哈希与迁移,任一环节被并发干扰,都可能造成节点“消失”或“跳过”。
resize 的核心步骤本身就容易被打断
当 size 超过 threshold(capacity × 0.75)时,HashMap 触发扩容:创建新数组(容量翻倍),再把旧 table 中每个桶的节点逐个 rehash 并迁移到新表对应位置。
这个过程不是“整体锁住再搬”,而是按桶(bucket)逐个处理。关键问题就出在“单个桶内链表迁移”这一步:
- 线程 A 开始迁移某个桶的链表,刚处理完前两个节点,就被挂起
- 线程 B 同样迁移该桶,完整走完迁移逻辑,把全部节点搬到了新表
- 线程 A 恢复执行,继续用它“看到的旧链表头”往下搬——但此时链表已被线程 B 修改,A 的 next 指针可能指向已迁移/已断开的节点,导致部分节点被跳过或重复插入
- 最终结果:旧链表中某些节点既没进入新表,也没保留在旧表,彻底丢失
Java 7 的头插法放大了风险
JDK 1.7 使用头插法迁移链表:新节点总插入到链表头部。多线程并发迁移时,两个线程对同一链表做头插,极易因指针覆盖形成环形链表(如 A→B→A)。一旦成环:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 后续 get() 遍历该桶会无限循环,CPU 占满
- resize 过程中若遇到环,遍历中断,剩余节点不再迁移 → 数据丢失
虽然 JDK 8 改为尾插法,消除了成环可能,但 节点迁移仍非原子,上述“A 挂起、B 先搬完、A 继续搬错”问题依然存在。
size 计数不准加剧丢失表现
HashMap 的 size 是一个普通 int 字段,没有 volatile 或 CAS 保护。多线程并发 put 时:
- 每个线程执行 ++size 是读-改-写三步操作,非原子
- 即使所有节点都成功插入,size 最终值也可能小于实际数量
- 更严重的是:size 不准会误导扩容时机——本该扩容时没扩,桶链过长;不该扩时误扩,又触发一轮高风险 resize
这种计数偏差不直接“删数据”,但会让系统在错误时间点进入更脆弱的状态,间接放大丢失概率。
没有异常提示,丢失静默发生
HashMap 完全不检测并发修改。它不会抛 ConcurrentModificationException(那是 fail-fast 迭代器的机制),也不会校验链表完整性或数组一致性。
所以数据丢失是静默的:程序照常运行,日志无报错,只有业务侧发现 map.size() 小于预期、get() 返回 null 或 key 对不上——往往要到线上压测或流量高峰才暴露。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










