单线程调用 ArrayList.remove() 会抛 ConcurrentModificationException,因为迭代器与集合共享 modCount,遍历时外部修改会破坏 fail-fast 契约;正确做法是用 Iterator.remove()、removeIf() 或倒序索引删除。

为什么单线程调用 ArrayList.remove() 也会抛 ConcurrentModificationException
因为迭代器(Iterator)和集合本体共享同一个 modCount 计数器,只要在迭代过程中有非迭代器自身的修改操作,就会触发校验失败。这不是并发问题,而是“结构修改未走迭代器通道”的契约破坏。
常见错误现象:for (String s : list) { if (s.isEmpty()) list.remove(s); } —— 看似单线程、顺序执行,但底层 Iterator.next() 每次都会检查 modCount 是否被外部改动过。
- 使用场景:遍历中根据条件删除元素(比如过滤空字符串、清理过期任务)
-
ArrayList、HashMap、LinkedList等所有基于modCount的集合都遵循这一机制 - 不是 bug,是 fail-fast 设计的主动报错,避免产生不可预测的跳过或重复遍历
安全删除的三种写法及适用边界
核心原则:让修改动作由迭代器自己完成,或彻底避开迭代器。
- 用
Iterator.remove()—— 最常用,适用于需要判断后立即删的场景Iterator<string> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.isEmpty()) it.remove(); }</string> - 用
removeIf()(Java 8+)—— 简洁,但内部仍用迭代器,不支持中断或额外副作用list.removeIf(String::isEmpty); - 倒序
for循环 + 索引删除 —— 绕过迭代器,适合需精确控制索引或兼容老版本的场景for (int i = list.size() - 1; i >= 0; i--) { if (list.get(i).isEmpty()) list.remove(i); }
注意:removeIf() 在 CopyOnWriteArrayList 中不会抛异常,但它是线程安全的代价:每次修改都复制数组,单线程下性能反而更差。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
ConcurrentModificationException 不等于多线程竞争
这个异常名极具误导性。它只表示“检测到结构修改与当前迭代状态不一致”,和线程数无关。
- 即使整个方法加了
synchronized,只要用了增强for循环再调用list.remove(),照样抛 -
ConcurrentHashMap的迭代器不检查modCount,所以它的forEach()或keySet().iterator()中调用remove()不会抛这个异常(但可能看不到最新修改) - 真正多线程下,
ArrayList还有数据错乱、越界等更隐蔽的问题,ConcurrentModificationException反而是最友好的提示
哪些操作会触发 modCount 变更
所有改变集合结构的方法都会自增 modCount,包括:
-
add()、remove()、addAll()、clear() -
retainAll()、removeAll()(哪怕没实际删任何元素,也改了modCount) - 但
set()不会 —— 它只替换元素内容,不改变结构,所以for (int i = 0; i 是安全的
容易被忽略的是:某些工具方法(如 Collections.synchronizedList() 包装后,仍用原生 Iterator,不解决这个问题;必须用其返回的 iterator() 才能配合 remove())

















