collections.synchronizedset()仅保证单个方法原子性,复合操作和迭代需手动同步;正确做法是以该set实例为锁对象加synchronized块,遍历时也需同步或转数组;高并发场景推荐concurrenthashmap.newkeyset()或concurrentskiplistset。

直接用 Collections.synchronizedSet(new HashSet()) 能让单个 add、remove、contains 操作线程安全,但复合逻辑和迭代仍会出错——关键不在“包没包好”,而在“怎么用才不出问题”。
单方法安全,不代表整体安全
包装后的 Set 确保每个 public 方法(如 add()、contains())是原子的,但多个方法组合就不受保护。比如下面这段代码在多线程下可能重复添加:
-
if (!set.contains("x")) set.add("x");—— 判断和添加之间存在时间窗口,两个线程都通过判断后执行 add - 类似地,
removeIf、retainAll等批量操作也不具备原子性
复合操作必须加同步块
正确做法是用该同步 Set 实例本身作锁对象,不要另建新锁:
- ✅ 正确:
synchronized (syncSet) { if (!syncSet.contains("key")) syncSet.add("key"); } - ❌ 错误:
synchronized (new Object()) { ... }或synchronized (this) { ... }—— 锁对象不一致,不同步 - 注意:
syncSet内部的mutex字段不可见,但其自身就是锁对象,这是 Collections.synchronizedSet 的设计约定
遍历必须手动同步,否则抛异常
即使 Set 是同步的,它的迭代器不是线程安全的。并发修改+遍历大概率触发 ConcurrentModificationException:
- ✅ 安全遍历:
synchronized (syncSet) { for (String s : syncSet) { ... } } - ✅ 替代方案(读多写少且数据量小):
for (String s : syncSet.toArray()) { ... } - ❌ 危险操作:边遍历边调用
remove()或add(),除非整个操作都在同一synchronized块内
高并发场景建议换 ConcurrentSet
Collections.synchronizedSet 是全局独占锁,吞吐量低。如果业务读多写少、或并发压力大,优先考虑:
-
ConcurrentHashMap.newKeySet()(Java 8+):底层基于分段锁/CAS,支持无锁读、并发写,性能显著更好 -
ConcurrentSkipListSet:有序、并发安全,适合需要排序的场景 - 注意:
newKeySet()迭代器不保证强一致性(可能漏元素或看到旧值),但绝大多数业务可接受
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











