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
相关产品推荐
相关产品推荐

