You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java 17多线程环境下HashSet存两不等元素却仅留一个?原因剖析

多线程下HashSet的异常场景分析

这种情况确实可能发生,因为HashSet本身是非线程安全的,底层依赖HashMap实现,多线程并发操作会破坏其内部数据结构的一致性,最终出现你描述的现象。

结合Java 17中HashMap的实现(数组+链表/红黑树结构),以下是两种典型的事件序列:

场景一:数组节点覆盖

假设线程A添加值a,线程B添加值b,a.equals(b)返回false,但二者的hash值计算后对应数组的同一索引i:

  • 初始状态:HashMap的底层数组table中索引i的位置为空。
  • 线程A执行add(a):完成hash值计算和索引定位,确认table[i]为空,准备创建Node(a, PRESENT)节点并赋值给table[i],但在赋值前被挂起。
  • 线程B执行add(b):计算出相同的索引i,此时table[i]仍为空(A未完成赋值),于是创建Node(b, PRESENT)节点并成功赋值给table[i],put方法返回null,因此线程B的add调用返回true。
  • 线程A恢复执行:直接将自己创建的Node(a, PRESENT)赋值给table[i],覆盖了线程B刚写入的节点。此时A的put方法返回null,所以add调用也返回true。
  • 最终结果:table[i]仅保留Node(a),值b被覆盖丢失,集合中只有a。

场景二:链表节点覆盖

假设数组索引i位置已有一个节点,线程A和线程B同时往该链表尾部插入新节点:

  • 线程A执行add(a):遍历到链表尾部,准备将尾部节点的next指针指向新创建的Node(a),但在修改指针前被挂起。
  • 线程B执行add(b):同样遍历到链表的原尾部节点(A未修改指针),成功将尾部节点的next指针指向Node(b),完成插入,add调用返回true。
  • 线程A恢复执行:直接将原尾部节点的next指针指向Node(a),覆盖了线程B设置的指针,导致Node(b)脱离链表,无法被HashMap的遍历逻辑访问到。
  • 最终结果:集合中仅能找到a,b丢失。

上述场景的核心原因是:HashMap的put操作并非原子操作,从计算索引、检查节点到修改数据结构的多个步骤,都没有同步保护,多线程并发时会出现竞态条件,导致一个线程的操作覆盖另一个线程的结果。

内容的提问来源于stack exchange,提问作者Sergey Zolotarev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 20:38:10