Java中保障HashSet线程安全有三种方式:1. Collections.synchronizedSet包装,单操作原子但迭代需手动同步;2. CopyOnWriteArraySet,读无锁、写复制,适合读多写少;3. ConcurrentHashMap.keySet(),并发性能优,不支持null。

Java 中的 HashSet 默认不是线程安全的,因为它底层直接复用 HashMap 的非同步实现,多个线程并发调用 add()、remove() 或遍历时,容易出现元素丢失、数据不一致,甚至抛出 ConcurrentModificationException。
用 Collections.synchronizedSet 包装
这是最常用、兼容性最好的方式,适用于读多写少、且迭代频率不高的场景。
- 调用
Collections.synchronizedSet(new HashSet())返回一个装饰后的线程安全 Set - 所有单个操作(如
add、contains、size)自动加锁,保证原子性 - 但迭代器本身不安全:必须手动对整个遍历块加同步,否则仍可能出错
示例:
Set<String> safeSet = Collections.synchronizedSet(new HashSet<>());
// 正确遍历方式
synchronized (safeSet) {
for (String s : safeSet) {
System.out.println(s);
}
}
用 CopyOnWriteArraySet 替代
适合读远多于写、且能接受写操作开销和弱一致性语义的场景(例如监听器列表、配置白名单)。
立即学习“Java免费学习笔记(深入)”;
- 内部基于
CopyOnWriteArrayList实现,每次写操作都复制底层数组 - 迭代过程完全无锁,不会抛
ConcurrentModificationException - 写操作较重(尤其集合大时),且迭代期间看不到其他线程刚做的修改(快照语义)
示例:
Set<String> safeSet = new CopyOnWriteArraySet<>();
safeSet.add("a");
safeSet.add("b");
// 可直接遍历,无需额外同步
for (String s : safeSet) {
System.out.println(s);
}
按需选用 ConcurrentHashMap 配合 keySet
如果业务中还需频繁查值、或未来可能扩展为键值结构,可直接用 ConcurrentHashMap,再通过 keySet() 获取线程安全的视图。
-
ConcurrentHashMap.keySet()返回的 Set 是线程安全的,支持并发读写 - 比
CopyOnWriteArraySet写性能更好,比synchronizedSet并发度更高 - 注意:该 Set 不允许
null元素(因 CHM 不支持 null key)
示例:
ConcurrentHashMap<String, Boolean> map = new ConcurrentHashMap<>();
Set<String> safeSet = map.keySet(); // 这个 set 是线程安全的
safeSet.add("x"); // 等价于 map.put("x", true)
safeSet.contains("x"); // 等价于 map.containsKey("x")
避免常见误区
别以为“只读”就绝对安全——若其他线程正在写,未同步的读仍可能看到过期值;也别在循环中仅对单个 add() 加锁,而忽略迭代逻辑。
- 不要混用原始 HashSet 和包装后的引用(如把
new HashSet()直接赋给多个变量) - 不要依赖
synchronizedSet的迭代器做条件删除(如边遍历边remove()),应改用removeIf()或先收集待删项 - 高并发、低延迟场景慎用
synchronizedSet,其全局锁会成为瓶颈


















