1. 什么是 ConcurrentModificationException?
它是 Java 集合框架抛出的运行时异常,表示集合在遍历过程中被结构性修改了,导致迭代器无法保证一致性。
2. 异常触发的底层机制
Java 集合(如 ArrayList、HashSet 等)在创建迭代器时,会记录集合的结构性修改次数(modCount)。每次集合结构发生变化(如 add、remove),modCount 增加。迭代器内部有一个 expectedModCount,每次调用 next()/hasNext() 时,会检查 modCount 是否和 expectedModCount 一致。如果不一致,就抛出 ConcurrentModificationException。
3. 导致 ConcurrentModificationException 的所有常见原因
3.1 遍历过程中直接修改集合
错误代码示例:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) {
list.remove(s); // 错误!遍历时直接修改集合
}
}
原因: for-each 底层用的是 iterator,直接用 list.remove() 修改集合,modCount 改变,expectedModCount 没变,抛异常。
3.2 用 Iterator 遍历时,直接用集合的 add/remove 方法修改集合
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("b")) {
list.remove(s); // 错误!应使用 it.remove()
}
}
3.3 多线程并发修改集合
一个线程遍历集合,另一个线程同时修改集合(结构性操作),也会抛异常。
示例:
List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
new Thread(() -> {
for (Integer i : list) {
System.out.println(i);
}
}).start();
new Thread(() -> {
list.add(4); // 并发修改
}).start();
3.4 在 for-each 循环中调用集合的 remove/add
for (String s : list) {
list.remove(s); // 错误
}
正确做法: 用 Iterator 的 remove 方法。
3.5 对 Map 的 keySet/values/entrySet 进行遍历时修改 Map
Map<String, Integer> map = new HashMap<>();
map.put("a", 1); map.put("b", 2);
for (String key : map.keySet()) {
map.remove(key); // 错误
}
3.6 迭代器遍历时,集合结构被外部方法修改
如果在遍历过程中调用了会修改集合结构的方法(即使不是直接在循环体里),也会导致异常。
3.7 使用 fail-fast 集合(如 ArrayList、HashSet、HashMap)遍历时结构性修改
Java 的大多数集合都是 fail-fast 的(快速失败),即检测到并发修改就立即抛异常。
4. 什么是结构性修改?
结构性修改指的是影响集合元素数量或排列的操作,比如 add、remove、clear、put(Map),而仅仅修改元素内容(如 set(index, value))不算结构性修改。
5. 如何避免 ConcurrentModificationException?
- 用 Iterator 的 remove 方法删除元素
Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("b")) { it.remove(); } } - 用并发安全集合
如CopyOnWriteArrayList、ConcurrentHashMap,这些集合不会抛 ConcurrentModificationException。 - 遍历前收集要删除的元素,遍历后统一删除
List<String> toRemove = new ArrayList<>(); for (String s : list) { if (条件) toRemove.add(s); } list.removeAll(toRemove); - 使用 ListIterator 的 add/remove/set 方法
6. 其他补充
ConcurrentModificationException只是 fail-fast 机制的一部分,不能保证百分百检测到所有并发修改。- 在多线程环境下,优先使用并发集合或加锁处理。
7. 总结
所有原因本质:
遍历过程中集合结构被直接或间接修改,导致迭代器检测到不一致。
常见场景:
- for-each 循环中直接 add/remove
- Iterator 遍历时集合 add/remove
- 多线程并发修改
- Map 的 keySet/values/entrySet 迭代时修改 Map
避免方式:
- 用 iterator.remove()
- 用并发集合
- 遍历前收集、后批量修改
8. 底层原理再深入
8.1 modCount 和 expectedModCount
- 每个 fail-fast 集合(如 ArrayList、HashMap)内部有一个
modCount字段,代表结构性修改次数。 - 创建迭代器时,迭代器保存一份
expectedModCount。 - 每次迭代器操作(如
next()、remove())时,都会检查modCount是否和expectedModCount一致。 - 如果不一致,说明集合被外部修改,抛出
ConcurrentModificationException。
源码片段(以 ArrayList 为例):
public E next() {
checkForComodification(); // 检查
...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
9. 特殊集合的处理
9.1 并发集合不会抛出该异常
CopyOnWriteArrayList、ConcurrentHashMap等并发集合,内部机制不同,不会 fail-fast。- 例如,
CopyOnWriteArrayList每次修改都会复制一份新数据,迭代器遍历的是旧快照,不关心后续修改。
9.2 老的 Vector、Hashtable
- 这些集合的方法都加了同步锁(synchronized),不会抛出
ConcurrentModificationException,但性能较低。
10. 实际项目中的规避策略
10.1 单线程环境
- 删除元素用 iterator.remove()。
- 遍历前收集需要删除的元素,遍历后统一删除。
- 不要在 for-each 或普通 for 循环中直接 remove/add。
10.2 多线程环境
- 使用并发集合(如 CopyOnWriteArrayList、ConcurrentHashMap)。
- 使用同步块(synchronized)保护遍历和修改操作。
- 分批处理:先收集需要修改的数据,后统一操作。
10.3 遍历 Map 的安全删除
- 用
Iterator<Map.Entry<K,V>>遍历,然后用 iterator.remove() 删除当前 entry。
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> entry = it.next();
if (entry.getValue() < 10) {
it.remove();
}
}
11. 排查和调试方法
11.1 查看异常堆栈
- 异常堆栈会指向集合的迭代器方法(如 next()),分析调用链,定位哪里修改了集合。
11.2 检查所有集合修改点
- 搜索代码中所有对集合的结构性操作(add、remove、clear等),看是否在遍历期间被调用。
11.3 多线程场景
- 检查是否有线程并发修改集合,必要时加锁或用并发集合。
12. 面试延伸问题
- 什么是 fail-fast?什么是 fail-safe?举例说明。
- fail-fast:检测到并发修改立即抛异常(如 ArrayList)。
- fail-safe:迭代器遍历的是快照,不抛异常(如 CopyOnWriteArrayList)。
- 如何安全地在遍历过程中删除集合元素?
- 用 iterator.remove()。
- ConcurrentModificationException 一定能检测到所有并发修改吗?
- 不能,只能检测到部分典型场景。
- 为什么并发集合不会抛 ConcurrentModificationException?
- 并发集合设计了特殊机制,如快照、分段锁等,保证遍历安全。
13. 真实案例分析
案例:批量删除数据库记录时同步维护缓存集合
假设你有一个缓存 List,批量删除数据库记录后也要同步删除 List 中的元素:
// 错误做法,可能抛异常
for (User user : cacheList) {
if (shouldDelete(user)) {
cacheList.remove(user);
}
}
// 正确做法
Iterator<User> it = cacheList.iterator();
while (it.hasNext()) {
User user = it.next();
if (shouldDelete(user)) {
it.remove();
}
}
14. 代码示例:多线程并发修改
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) list.add(i);
Thread t1 = new Thread(() -> {
for (int i : list) {
// 遍历
}
});
Thread t2 = new Thread(() -> {
list.remove(50); // 并发修改
});
t1.start(); t2.start();
// 可能抛 ConcurrentModificationException
15. 总结
- ConcurrentModificationException 是集合 fail-fast 机制的体现。
- 本质原因是遍历期间结构性修改集合。
- 规避方法:用 iterator.remove(),用并发集合,多线程加锁,遍历后统一修改。
- 多线程场景优先用并发集合,普通集合加锁也可。
- 面试常问底层原理、fail-fast 与 fail-safe、实际场景规避。
730

被折叠的 条评论
为什么被折叠?



