Collections.synchronizedSet()仅保证单方法原子性,复合操作如“检查后添加”仍需手动同步;正确做法是用该集合实例作为锁对象,或改用ConcurrentHashMap.newKeySet()提升并发性能。

为什么不能直接用 Collections.synchronizedSet(new HashSet()) 就完事?
它确实能返回一个线程安全的 Set,但「线程安全」只覆盖单个方法调用——比如 add()、contains() 是原子的;一旦涉及复合操作(如“检查不存在再添加”),仍需手动同步。常见误用是写 if (!set.contains(x)) set.add(x);,这在多线程下会重复添加。
正确包装并确保复合操作安全的写法
必须配合外部 synchronized 块,锁对象得用包装后集合的 mutex 字段(Collections.synchronizedSet() 内部维护的)。
- 获取 mutex:包装后的集合本身不暴露 mutex,但其内部类持有,所以应直接用该集合实例作为锁对象
- 示例:
Set<string> syncSet = Collections.synchronizedSet(new HashSet());<br>// 安全的“若不存在则添加”<br>synchronized (syncSet) {<br> if (!syncSet.contains("key")) {<br> syncSet.add("key");<br> }<br>}</string> - 别用
new Object()或其他任意对象做锁,否则不同步
比 synchronizedSet() 更适合高并发场景的替代方案
Collections.synchronizedSet() 是粗粒度锁,整个集合一把锁,吞吐量低。如果读多写少或要求更高并发性,优先考虑:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
-
ConcurrentHashMap.newKeySet()(Java 8+):底层基于ConcurrentHashMap,支持无锁读、分段写,性能好得多 - 示例:
Set<string> concurrentSet = ConcurrentHashMap.newKeySet();<br>// 下面这行是线程安全的,无需额外同步<br>concurrentSet.add("item");</string> - 注意:
ConcurrentHashMap.newKeySet()不保证迭代器强一致性(可能漏掉或重复元素),但大多数业务场景可接受
容易被忽略的迭代陷阱
即使用了 Collections.synchronizedSet(),遍历仍可能抛 ConcurrentModificationException,因为迭代器本身不是同步的。
- 必须在同步块中迭代:
synchronized (syncSet) {<br> for (String s : syncSet) {<br> // ...<br> }<br>} - 或者转成数组再遍历(适合读多写少且数据量不大):
syncSet.toArray() - 千万别边遍历边修改(如
removeIf()),除非也在同一把锁里
同步的真正难点不在包装,而在识别哪些操作是原子的、哪些需要扩展锁范围——尤其是迭代和条件更新这类看似简单却极易出错的地方。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










