HashSet线程不安全,因其底层依赖非线程安全的HashMap;解决方案有三:一是Collections.synchronizedSet包装,需手动同步迭代;二是CopyOnWriteArraySet,读多写少场景适用;三是改用ConcurrentHashMap或加锁等替代方案。

HashSet 在多线程环境下直接使用会出问题,最典型的是抛 `ConcurrentModificationException`,根本原因是它底层用的是非线程安全的 `HashMap`,而 `add()` 等操作没加锁,多个线程同时读写就会冲突。
为什么 HashSet 会线程不安全
HashSet 本身不存数据,它把所有元素作为 key 存进内部的 HashMap,value 固定是同一个静态对象(`PRESENT`)。所以它的线程安全性完全取决于 HashMap —— 而 HashMap 没做同步,扩容、put、迭代都可能被并发打断。哪怕只是 2~3 个线程同时 add,也容易触发异常或数据丢失。
方案一:用 Collections.synchronizedSet 包装
这是最直接的兼容性方案,适合已有代码改动小、读多写少的场景:
- 调用 Collections.synchronizedSet(new HashSet()) 获取线程安全包装类
- 注意:迭代时必须手动同步整个集合,否则仍可能出错
- 示例:synchronized(set) { for (String s : set) { ... } }
- 缺点:所有方法串行执行,高并发下性能瓶颈明显
方案二:用 CopyOnWriteArraySet 替代
适用于读远多于写的场景(比如监听器列表、配置白名单):
- 底层基于 CopyOnWriteArrayList,每次 add 都复制数组,写操作加锁,读操作完全无锁
- 创建简单:new CopyOnWriteArraySet()
- 优点:遍历绝对安全,不怕并发修改
- 缺点:内存开销大、写性能差,不适合频繁增删或大数据量
方案三:考虑是否真需要 Set 语义
有时业务上“去重”不等于必须用 Set,可换思路:
- 如果只是防止重复添加,可用 ConcurrentHashMap 的 key 做替代(putIfAbsent)
- 如果要保证强一致性且并发高,可配合 ReentrantLock 手动控制临界区
- 若数据量小、线程数可控,加一层 synchronized 方法也能快速兜底











