商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何在 Java 中使用 CopyOnWriteArraySet 确保在并发修改时不会抛出迭代器异常

如何在 Java 中使用 CopyOnWriteArraySet 确保在并发修改时不会抛出迭代器异常

  发布于2026-05-23 阅读(0)

扫一扫,手机访问

CopyOnWriteArraySet:迭代器不抛异常的真相与代价

如何在 Ja va 中使用 CopyOnWriteArraySet 确保在并发修改时不会抛出迭代器异常

提到CopyOnWriteArraySet,很多开发者第一反应就是:“哦,那个迭代时不会抛ConcurrentModificationException的线程安全集合。”这话没错,但只说对了一半。更准确地说,它的迭代器返回的是一个弱一致性的历史快照,而非实时视图。这种设计常被误读为“完全安全”,殊不知背后藏着几个关键限制,用不好反而会引入更隐蔽的逻辑问题。

为什么遍历时不会抛 ConcurrentModificationException?

根本原因在于,CopyOnWriteArraySetiterator()方法返回的,是迭代器被构造那一刻集合内容的不可变副本。其底层基于CopyOnWriteArrayList实现,核心机制就四个字:写时复制

每次执行写操作(比如addremove),它都会将底层的整个数组复制一份,在副本上完成修改,最后再原子性地替换掉旧的数组引用。而迭代器自始至终遍历的,都是那个旧的、不变的数组副本。读写操作在物理上就分道扬镳了,自然不会有修改冲突。

不过,这种优雅的隔离是有代价的:

  • 写操作开销不容小觑:频繁的增删会触发大量的数组复制,数据量一大,CPU和内存的压力立刻显现。
  • 迭代器活在“过去”:你在迭代过程中调用addremove,当前迭代器是绝对感知不到的,它看到的永远是那个“旧世界”。
  • 迭代中删除行不通:它的Iterator.remove()方法直接抛出UnsupportedOperationException,想边遍历边清理?此路不通。

什么时候才适合用 CopyOnWriteArraySet?

它并非通用解药,而是为特定场景量身定制的工具。简单来说,就是读多写少,且对读取的实时性要求不高

典型的适用场景包括:监听器注册表、配置项或白名单的缓存。在这些场景里:

  • 读操作占绝对主导(比如超过95%),写操作极少,通常只在初始化或偶发配置更新时发生。
  • 业务能接受“弱一致性”:例如,在广播事件前,快照出所有监听器进行通知,之后新增或移除的监听器不影响本次广播,这是完全合理的。
  • 集合规模适中:元素数量最好稳定在几百以内。一旦超过上千,单次add操作带来的数组复制成本就会变得非常显著。

替代方案对比:ConcurrentHashMap.newKeySet() vs CopyOnWriteArraySet

如果你的场景是高频写入,同时又需要安全的迭代,那么ConcurrentHashMap.newKeySet()(Ja va 8及以上)往往是更合适的选择。

立即学习“Ja va免费学习笔记(深入)”;

  • 性能更平稳ConcurrentHashMap.newKeySet()返回的Set支持高并发修改,其迭代器也是弱一致性的,但它通过分段锁等机制,避免了复制整个数据结构,性能表现更可预测。
  • 功能更灵活:它允许null元素,而CopyOnWriteArraySet不允许。
  • 仍需注意一致性:当然,newKeySet()的迭代器也可能跳过迭代过程中的某些写入(取决于锁的时机),但它绝不会因为并发修改而抛出异常。
  • 强一致性的选择:如果业务逻辑要求迭代器必须看到所有已提交的写入(强一致性),那就只能回归传统方案:使用Collections.synchronizedSet()包装,并在迭代时手动用synchronized块对整个集合进行加锁保护。

一个典型误用示例与修复思路

来看一段看似安全、实则存在逻辑问题的代码:

CopyOnWriteArraySet set = new CopyOnWriteArraySet<>(Arrays.asList("a", "b"));
for (String s : set) {
    if ("a".equals(s)) {
        set.remove("b"); // ✅ 这行不会抛异常,但本次循环仍会输出 "b"
    }
}
// 输出:a b —— remove操作确实生效了,但迭代器看不到

问题在于,迭代器遍历的是旧快照,所以即使删除了“b”,当前的循环依然会处理它。如果业务依赖“边遍历边清理”的逻辑,这就出错了。

修复方案通常有几个方向:

  • 批量处理:先遍历收集需要删除的元素,结束后再调用removeAll一次性删除。这适用于写操作不频繁的场景。
  • 换用弱一致性容器:改用ConcurrentHashMap.newKeySet(),并明确接受其弱一致性的语义。
  • 使用专用队列:对于典型的“生产-消费”模式,直接使用ConcurrentLinkedQueue这类无锁队列配合Iterator,可能是更清晰的选择。

说到底,在并发编程中,真正棘手的往往不是抛出的异常,而是“没有异常,但结果错了”CopyOnWriteArraySet通过快照机制屏蔽了并发修改异常,却也容易掩盖更深层的竞态条件逻辑缺陷。理解其语义,方能善用其利,规避其弊。

本文转载于:https://www.php.cn/faq/2420569.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注