ArrayList非线程安全,多线程遍历修改会触发ConcurrentModificationException;应选用CopyOnWriteArrayList、Collections.synchronizedList或手动加锁,或改用只读遍历+批量修改、Iterator.remove()、不可变集合等方案规避。

ArrayList 本身不是线程安全的,多线程环境下直接读写(尤其是边遍历边修改)会触发 ConcurrentModificationException。这不是“偶尔出错”,而是其 fail-fast 机制的主动保护——它通过 modCount 计数器检测结构被意外修改,从而抛异常中断错误行为。解决问题的关键不在于“捕获异常”,而在于从设计层面规避并发冲突。
用线程安全的替代集合
最直接的方式是替换为 JDK 提供的线程安全集合:
- CopyOnWriteArrayList:适合读多写少场景(如监听器列表、配置项缓存)。每次写操作复制整个数组,读操作无锁且不会触发 CME;但内存开销大、写性能差、迭代时看不到最新写入的数据。
-
Collections.synchronizedList(new ArrayList()):底层加了对象锁,所有方法串行执行。读写都安全,但迭代仍需手动同步(否则仍可能 CME),例如:
synchronized (list) { for (E e : list) { ... } }
手动加锁控制访问临界区
如果必须用 ArrayList,且对性能或语义有特殊要求(比如需要精确控制锁粒度),可用显式锁或 synchronized 包裹所有涉及修改和遍历的操作:
- 将增删改查、以及 for-each / iterator 遍历统一放在同一个锁对象下;
- 避免在锁内做耗时操作(如 I/O、远程调用),防止阻塞其他线程;
- 若多个 ArrayList 逻辑上属于同一业务单元(如用户订单列表 + 订单状态映射),建议共用一个锁对象,而非各自加锁。
避免遍历时修改 —— 改变使用方式
很多 CME 其实源于编码习惯问题。以下做法可彻底绕过问题:
- 遍历时只读,把要删除/添加的元素先收集到另一个集合(如
ArrayList<Integer> toRemove = new ArrayList<>()),遍历完再统一处理; - 使用
Iterator.remove()安全删除当前元素(这是唯一允许的遍历中修改方式); - 用增强 for 循环时,绝不要在循环体内调用
list.add()或list.remove(); - 批量修改优先用
list.replaceAll()、list.removeIf()等内部已同步的方法(它们自己管理 modCount)。
使用不可变集合(适合配置类场景)
如果列表初始化后不再变更(如系统常量、枚举映射),可转为不可变集合,从根本上杜绝并发修改可能:
- JDK 10+:用
List.copyOf(originalList); - Guava:
ImmutableList.copyOf(list); - 不可变集合线程安全、节省内存(共享内部数组)、且任何修改操作都会立即抛
UnsupportedOperationException,便于早期发现问题。

















