Collections.synchronizedSet()包装HashSet可解决基础并发问题,但需注意:必须手动同步迭代和复合操作,避免直接操作原始集合,且粗粒度锁影响高并发性能,应依读写比例选择CopyOnWriteArraySet或ConcurrentHashMap.keySet()。

直接用 Collections.synchronizedSet() 包装 HashSet,确实能解决基础的并发添加、删除问题,但真正用好它,关键不在“怎么创建”,而在“怎么用不踩坑”。它不是开箱即用的万能锁,而是一把需要你亲手握紧、时刻注意握法的工具。
同步Set的创建与基本用法
它本质是装饰器模式:把普通 Set(如 HashSet)套一层壳,所有 public 方法(add、remove、contains 等)内部自动加 synchronized 锁,锁对象就是包装后的集合实例本身。写法很简单:
- 推荐一步到位:
Set<string> safeSet = Collections.synchronizedSet(new HashSet());</string> - 变量建议声明为 final,防止被意外替换或暴露底层原始引用
- 不要在包装后还去操作原始 Set(比如先
HashSet raw = new HashSet(),再synchronizedSet(raw),然后又用raw.add(...)),那样锁完全失效
迭代必须手动加锁,否则会出错
这是最常被忽略的致命点:synchronizedSet 保证单个方法线程安全,但 Iterator 和 for-each 不受保护。多线程一边遍历、一边修改,大概率触发 ConcurrentModificationException 或读到脏数据。
- 正确做法是整个遍历块用
synchronized(safeSet)包裹 - 例如:
synchronized (safeSet) { for (String s : safeSet) { System.out.println(s); } } - 哪怕只读不写,只要其他线程可能在改,就必须加锁——因为迭代器本身不是线程安全的
复合操作需整体同步,单个方法不够用
像“如果不存在就添加”这种逻辑,contains() 和 add() 是两个独立调用,中间可能被其他线程插入操作,导致重复添加或漏加。
立即学习“Java免费学习笔记(深入)”;
- 必须把整段逻辑放在同一个同步块里:
synchronized (safeSet) { if (!safeSet.contains("key")) { safeSet.add("key"); } } - 类似场景还有:检查大小后清空、根据条件批量移除等,凡涉及多个步骤判断+操作,都要整体同步
性能与替代方案的选择依据
synchronizedSet 是粗粒度锁——所有操作共用一把锁。高并发写场景下容易成为瓶颈,吞吐量上不去。
- 读远多于写的场景,
CopyOnWriteArraySet更合适:读操作无锁,写操作复制数组,适合监听器列表、配置白名单等 - 读写都频繁且要求高性能,优先考虑
ConcurrentHashMap.keySet()(它返回的 Set 视图本身就是线程安全的,支持高并发) - 简单共享缓存、低并发配置类场景,synchronizedSet 足够轻量、易理解,不必过度设计


















