hashset线程不安全,可用collections.synchronizedset(需手动同步迭代)、concurrenthashmap.newkeyset(高并发推荐)或copyonwritearrayset(读多写少)解决;复合操作需手动细粒度同步。

Java 中的 HashSet 本身不是线程安全的,多个线程同时读写时容易出现 ConcurrentModificationException、数据丢失或结构损坏。解决的关键是根据实际读写比例、并发强度和一致性要求,选择合适的方式,而不是简单加锁。
用 Collections.synchronizedSet 包装
这是最直接、兼容性最好的方式,适用于读写频率不高、线程数不多的场景。
- 调用
Collections.synchronizedSet(new HashSet())得到一个同步包装器 - 所有 add/remove/contains 等方法内部已加 synchronized,单次操作是原子的
- ⚠️ 迭代必须手动同步:不加锁遍历仍可能出错
正确写法:
Set<string> syncSet = Collections.synchronizedSet(new HashSet());
// 遍历时要显式同步整个集合
synchronized (syncSet) {
for (String s : syncSet) {
System.out.println(s);
}
}</string>
用 ConcurrentHashMap.newKeySet()
适合高并发读写、对性能敏感的场景。它基于分段锁或 CAS,支持更高吞吐量。
- 返回的是
KeySetView,实现了Set接口,天然线程安全 - 无须额外同步,迭代也安全(不会抛
ConcurrentModificationException) - 底层使用哈希表,平均时间复杂度 O(1),比 synchronizedSet 更高效
示例:
Set<string> concurrentSet = ConcurrentHashMap.newKeySet();
concurrentSet.add("a");
concurrentSet.parallelStream().forEach(System.out::println); // 安全</string>
用 CopyOnWriteArraySet
适用于“读极多、写极少”的场景,比如配置监听器列表、事件订阅者集合。
- 每次 add/remove 都复制整个数组,写操作开销大,但读操作完全无锁
- 迭代绝对安全,不会因并发修改失败
- 注意:不保证实时可见性——新线程可能短暂看不到刚添加的元素(取决于内存模型和 JIT 优化)
适用例子:
Set<runnable> listeners = new CopyOnWriteArraySet(); // 监听器增删极少,触发遍历极多</runnable>
按需手动同步更细粒度的逻辑
如果只是部分操作需要强一致性(如“检查后添加”),包装类无法满足,就得自己控制同步范围。
- 避免直接锁整个 Set,可改用私有锁对象减少争用
- 把复合操作(如 if-not-exists-then-add)放在同一同步块中
- 注意不要在同步块里调用外部不可控代码,防止死锁
例如原子添加(避免重复):
private final Set<string> set = Collections.synchronizedSet(new HashSet());
private final Object lock = new Object();
public boolean addIfAbsent(String s) {
synchronized (lock) {
if (!set.contains(s)) {
return set.add(s);
}
return false;
}
}</string>Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











