hashset非线程安全,多线程add会导致数据覆盖或cpu飙升;应改用concurrenthashmap模拟set、collections.synchronizedset或copyonwritearrayset,并校验add返回值实现幂等。

HashSet 本身不支持并发写入,直接多线程调用 add() 会导致数据覆盖、状态错乱,甚至在 JDK 1.7 等旧版本中引发环形链表和 CPU 飙高。根本原因在于:HashSet 底层用的是 HashMap,而 HashMap 的 put 操作不是原子的,扩容、哈希冲突处理等过程都缺乏线程同步保障。
用线程安全的替代实现
最直接有效的办法是换掉非线程安全的 HashSet:
-
ConcurrentHashMap + key-only 使用:创建
ConcurrentHashMap<e boolean></e>或ConcurrentHashMap<e object></e>,把元素作为 key 存入,value 固定用Boolean.TRUE或PRESENT;putIfAbsent(key, TRUE)返回null表示首次添加成功,否则说明已存在——这既避免覆盖,又天然线程安全。 -
Collections.synchronizedSet():包装成同步 Set,如
Collections.synchronizedSet(new HashSet());但要注意:它只保证单个方法(如 add、contains)原子,复合操作(如“先检查再添加”)仍需手动加锁。 -
CopyOnWriteArraySet:适合读多写少场景;每次写入都复制整个数组,开销大,但迭代时不会抛
ConcurrentModificationException,也不怕写入冲突。
校验 add() 的返回值,主动控制逻辑
即使用了线程安全集合,也要依赖 add() 的 boolean 返回值判断是否真正插入成功,不能忽略它:
-
if (!set.add(item)) { /* 已存在,跳过或报错 */ }是幂等操作的基础;忽略返回值,就等于放弃对重复写入的识别能力。 - 在任务调度、用户注册、事件去重等场景中,这个判断能防止业务层面的数据污染,比事后修复更可靠。
避免共享可变 HashSet 实例
很多问题源于多个线程共用同一个 HashSet 实例且无保护:
- 不要在类字段中声明
private Set<string> cache = new HashSet();</string>并让多个线程直接调用其方法。 - 若必须用 HashSet,至少确保每个线程操作的是独立实例(如局部变量),但这无法解决共享去重需求。
- 初始化时指定合理初始容量(如
new HashSet(1024)),可减少并发扩容概率,但不能消除线程不安全本质。
配合外部同步机制(慎用)
仅当无法更换集合类型、且写入频率极低时才考虑:
- 用
synchronized(set)包裹所有对 set 的读写操作,包括add()、contains()、remove()等。 - 注意:synchronized 锁的是对象引用,必须确保所有线程锁的是同一个对象;若 set 被重新赋值(如
set = new HashSet()),旧锁失效,极易出错。











