concurrentmodificationexception是fail-fast机制的警告信号,非线程安全集合遍历时被修改即抛出;copyonwritearraylist适用于读多写少场景,但写操作复制数组开销大且迭代器不支持remove。

并发修改异常(ConcurrentModificationException)不是线程安全问题的“报错”,而是快速失败(fail-fast)机制的警告信号——它说明你正在用非线程安全的集合(比如 ArrayList)在多线程环境下边遍历边修改。
为什么 ArrayList 迭代时增删会抛 ConcurrentModificationException
因为 ArrayList 的迭代器在构造时会记录当前的 modCount(结构修改次数),每次调用 next() 或 remove() 前都会校验该值是否被外部修改过。一旦发现不一致(比如另一个线程调用了 add()),立刻抛出异常。
这不是“并发导致数据错乱”的保护,而是“避免继续执行不可预期逻辑”的防御性中断。
- 单线程下也会触发:比如用
for-each遍历时,直接调用list.remove(obj) - 异常发生在
Iterator.next()时,不是remove()调用那一刻 - 它和真正的竞态条件(race condition)无关,但常是线程不安全操作的表征
CopyOnWriteArrayList 什么时候能用、什么时候不能用
CopyOnWriteArrayList 是 JDK 提供的线程安全列表实现,核心策略是:写操作(add、set、remove)时复制整个底层数组;读操作(get、iterator)无锁、直接访问当前数组快照。
立即学习“Java免费学习笔记(深入)”;
- ✅ 适合读多写少场景:比如监听器列表、配置项缓存、状态广播队列
- ❌ 不适合写频繁或数据量大场景:每次写都复制数组,GC 压力大,且新写入对正在遍历的迭代器不可见
- ⚠️ 迭代器不支持
remove():Iterator.remove()直接抛UnsupportedOperationException - ⚠️
size()和contains()返回的是快照值,可能和“最新状态”有延迟
示例:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
// 这个迭代器看到的是 ["a", "b"]
Iterator<String> it = list.iterator();
list.add("c"); // 写操作触发复制,但 it 仍指向旧数组
while (it.hasNext()) {
System.out.println(it.next()); // 只输出 a、b,看不到 c
}
替代方案比盲目用 CopyOnWriteArrayList 更重要
很多场景下,CopyOnWriteArrayList 是“治标不治本”的选择。真正要解决的,是设计层面的并发访问模式。
- 如果只是需要线程安全地收集回调函数,优先考虑
Collections.synchronizedList(new ArrayList())+ 显式同步块控制遍历 - 如果读写比例接近,或需强一致性,用
ConcurrentLinkedQueue或ConcurrentHashMap替代列表语义 - 如果必须用列表且允许最终一致性,可配合
ReentrantReadWriteLock手动控制读写分离 - Java 17+ 中,考虑
Vector已基本废弃,synchronizedList也仅适用于简单封装,别依赖其性能
容易被忽略的关键细节
CopyOnWriteArrayList 的迭代器是弱一致性的,但它的“弱”不是 bug,而是明确的设计契约。这意味着:
- 遍历时新增元素一定不可见,删除旧元素也不会影响当前迭代器(即使那个元素已被其他线程删掉)
-
toArray()返回的是调用时刻的快照,但返回的数组内容不会随原列表变化而变 - 它不保证不同线程间操作的 happens-before 关系,如需严格顺序,仍得靠
volatile或显式同步 - 构造函数传入普通
List时,会一次性复制全部元素——注意源列表是否正在被其他线程修改
真正难的从来不是选哪个类,而是判断“这里到底需不需要实时可见”“遍历中途丢几个新元素是否可接受”“写操作延迟几百微秒会不会破坏业务逻辑”。这些,没法靠换一个类自动解决。










