CopyOnWriteArraySet线程安全但写操作昂贵,基于COW机制实现,适合读多写少场景;add/remove为O(n),contains非哈希查找,不支持null,迭代器基于快照。

CopyOnWriteArraySet 是线程安全的,但别在高频写场景用
它底层用 CopyOnWriteArrayList 实现去重,每次增删都复制整个数组——读快、写贵。适合读多写少、且写操作不频繁的场景,比如监听器列表、配置白名单缓存。
常见错误现象:ConcurrentModificationException 不会出现,但你可能发现新增元素后立刻遍历却看不到,因为迭代器基于快照,而写操作已触发新数组生成。
- 写操作(
add()、remove())时间复杂度是 O(n),n 是当前集合大小 - 读操作(
contains()、遍历)无锁,但contains()是 O(n) 线性查找,不是哈希查找 - 不支持
null元素,添加会抛NullPointerException
add() 和 contains() 的行为和 ArrayList 不一样
CopyOnWriteArraySet.add() 本质是先查再插:调用 CopyOnWriteArrayList.addIfAbsent(),内部遍历比对 equals();而 contains() 同样靠遍历,不是常数时间。
对比 HashSet:没有哈希桶、不重写 hashCode() 也行,但性能差很多;如果你的元素重写了 hashCode() 和 equals(),又需要高性能去重,那它不是首选。
立即学习“Java免费学习笔记(深入)”;
- 即使元素实现了
Comparable,它也不排序,不保证迭代顺序(实际是插入顺序,但非强保证) -
add()返回boolean:true 表示新增成功,false 表示已存在(靠equals()判定) - 不要依赖
size()做条件判断后立即写入——中间可能有其他线程修改,但没关系,它本来就不承诺实时一致性
和 Collections.synchronizedSet() 的关键区别在哪
前者读不加锁、写全量复制;后者所有方法都套 synchronized,读写都串行。所以并发读压力大时,CopyOnWriteArraySet 吞吐更高;但写冲突多时,它会引发大量数组复制和 GC 压力。
典型误用:用它存用户在线状态并频繁更新心跳——每次 add() 都复制几千个用户对象,CPU 和内存很快顶不住。
-
Collections.synchronizedSet(new HashSet())支持null,CopyOnWriteArraySet不支持 - 前者迭代时若集合被改,会抛
ConcurrentModificationException;后者不会,但看到的是旧快照 - 前者可配合
ReentrantLock手动控制粒度;后者无法干预复制时机,完全黑盒
替代方案选型:什么情况下该换掉它
当出现以下任一情况,就该考虑换:add() 耗时明显上涨、GC 日志里 CopyOnWriteArraySet 相关数组分配飙升、或业务要求写操作必须低延迟。
- 读多写少 + 元素少(
- 读多写少 + 元素多 + 要求
contains()快 → 改用ConcurrentHashMap模拟 set:map.put(x, Boolean.TRUE) - 写频繁 + 需要强一致性 → 用
ReentrantLock包裹HashSet,自己控制临界区 - 需要有序 + 线程安全 →
Collections.synchronizedSortedSet(new TreeSet()),但注意迭代仍需手动同步
最易被忽略的一点:它的「线程安全」只针对单个操作原子性,不提供复合操作的原子保障。比如 if (!set.contains(x)) set.add(x) 依然存在竞态,必须用 add() 的返回值来判断,而不是靠两次单独调用。










