遍历中直接调用list.remove()会抛ConcurrentModificationException,因fail-fast机制检测到结构性修改;正确做法是用Iterator.remove()或removeIf()等安全方法。

直接在遍历中调用 list.remove() 会抛 ConcurrentModificationException
这是最常见也最容易踩的坑:用增强 for 循环或 iterator.next() 遍历时,直接对原集合调用 remove() 或 add(),JVM 会检测到结构性修改并立即抛出异常。底层靠 modCount 和 expectedModCount 对比实现,不是线程安全机制,而是快速失败(fail-fast)策略。
正确做法只有两种:
- 使用
Iterator的remove()方法(仅限单次删除) - 收集待删元素,遍历结束后再批量删除(如用
removeAll())
Listlist = new ArrayList<>(Arrays.asList("a", "b", "c", "b")); Iterator it = list.iterator(); while (it.hasNext()) { if ("b".equals(it.next())) { it.remove(); // ✅ 安全 } } // list 现在是 ["a", "c"]
CopyOnWriteArrayList 适合读多写少的并发场景,但别误用在普通循环里
CopyOnWriteArrayList 的写操作(add/remove)会复制整个数组,所以迭代器永远基于快照,不会抛 ConcurrentModificationException。但它不解决「逻辑一致性」问题——你在遍历时删元素,迭代器仍会继续遍历旧副本,后续逻辑可能出错。
典型误用:
立即学习“Java免费学习笔记(深入)”;
ListcowList = new CopyOnWriteArrayList<>(Arrays.asList(1, 2, 3, 4)); for (Integer x : cowList) { if (x == 3) cowList.remove(x); // ❌ 看似不报错,但下一轮循环仍会取到 4(原数组长度没变) }
它真正适用的场景是:监听器列表、配置项缓存等「极少修改、高频遍历」且能容忍短暂脏读的场合。普通业务逻辑中,优先选显式 Iterator.remove() 或重构为不可变操作。
批量修改优先用 removeIf() 和 replaceAll()(Java 8+)
这两个方法内部封装了安全的迭代逻辑,语义清晰、代码简洁,且避免手写 Iterator 的样板代码。
-
removeIf(predicate):遍历并移除匹配元素,等价于安全版的filter().collect() -
replaceAll(unaryOperator):原地更新每个元素,不改变集合结构
注意:removeIf() 对 ArrayList 是 O(n) 时间,但对 LinkedList 可能退化为 O(n²),因为每次删除都要重连节点;而 replaceAll() 在所有 List 实现中都是 O(n)。
Listlist = new ArrayList<>(Arrays.asList("hello", "world", "java")); list.removeIf(s -> s.length() < 5); // 移除 "java"(长度 4) list.replaceAll(String::toUpperCase); // 变成 ["HELLO", "WORLD"]
修改前先确认集合是否被 Collections.unmodifiableXXX() 包装过
运行时抛 UnsupportedOperationException 是这类包装集合的典型错误,不是并发问题,而是设计上禁止修改。常见来源包括:
Collections.unmodifiableList(list)-
Arrays.asList()返回的列表(底层是ArrayList子类,不支持增删) - Spring 等框架返回的只读配置集合
判断方式很简单:list.getClass().getName() 会暴露类似 java.util.Collections$UnmodifiableRandomAccessList 这样的类名;或者尝试 list.add(null) 看是否立刻抛异常。
若需修改,必须创建可变副本:new ArrayList(originalList) 或 new LinkedList(originalList),不能跳过这步直接 cast 或反射绕过。










