避坑指南:ConcurrentModificationException 的根源与解决方案
避坑指南:ConcurrentModificationException 的根源与解决方案
|
🌺The Begin🌺点点关注,收藏不迷路🌺
|
1. 引言:一个让无数程序员抓狂的异常
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String s : list) {
if ("B".equals(s)) {
list.remove(s); // ❌ 抛出 ConcurrentModificationException
}
}
有多少人写过类似的代码?又有多少次被这个突如其来的异常搞得手足无措?
本文将深入剖析 ConcurrentModificationException 的产生原理、触发场景以及最佳解决方案。
2. 一图看懂:异常产生的完整流程
3. 源码解析:fast-fail 机制的秘密
3.1 modCount 是什么?
ArrayList 继承自 AbstractList,其中有一个关键变量:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
// 记录集合结构性修改的次数
protected transient int modCount = 0;
}
结构性修改指的是改变集合大小的操作,包括:
add()- 添加元素remove()- 删除元素clear()- 清空集合addAll()- 批量添加retainAll()- 保留指定元素
非结构性修改(不改变大小)不会影响 modCount:
set()- 替换元素
3.2 迭代器的实现
// ArrayList 的内部迭代器类
private class Itr implements Iterator<E> {
int cursor; // 下一个元素的下标
int lastRet = -1; // 最后返回的元素下标
int expectedModCount = modCount; // 🔑 关键:期望的修改次数
public E next() {
checkForComodification(); // 每次 next 都会检查
int i = cursor;
// ... 获取元素
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); // 也会检查
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount; // 修正期望值
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 核心检查方法
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
3.3 异常触发流程图解
4. 常见触发场景及解决方案
4.1 场景一:遍历时直接删除
// ❌ 错误写法
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
if ("B".equals(s)) {
list.remove(s); // 抛出异常
}
}
✅ 解决方案一:使用迭代器的 remove 方法
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("B".equals(s)) {
it.remove(); // ✅ 正确:迭代器自己的 remove 方法
}
}
System.out.println(list); // [A, C]
✅ 解决方案二:倒序遍历
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (int i = list.size() - 1; i >= 0; i--) {
if ("B".equals(list.get(i))) {
list.remove(i); // ✅ 倒序删除不影响遍历
}
}
✅ 解决方案三:使用 removeIf(Java 8+)
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.removeIf(s -> "B".equals(s)); // ✅ 最简洁
4.2 场景二:增强 for 循环的本质
增强 for 循环只是语法糖,实际编译后还是迭代器:
// 源代码
for (String s : list) {
if ("B".equals(s)) {
list.remove(s);
}
}
// 编译后(等价代码)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("B".equals(s)) {
list.remove(s); // ❌ 调用了 List 的方法,不是 Iterator 的
}
}
4.3 场景三:多线程并发修改
// ❌ 多线程问题
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
Thread t1 = new Thread(() -> {
for (String s : list) {
System.out.println(s);
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
});
Thread t2 = new Thread(() -> {
try { Thread.sleep(5); } catch (InterruptedException e) {}
list.add("C"); // 另一个线程修改
});
t1.start();
t2.start();
✅ 解决方案:使用并发容器
// 方案1:CopyOnWriteArrayList(读多写少)
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
// 多线程安全,迭代器是快照
// 方案2:使用同步包装
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 但迭代时仍需手动同步
synchronized (list) {
Iterator<String> it = list.iterator();
while (it.hasNext()) {
it.next();
}
}
4.4 场景四:subList 的坑
// ❌ subList 与原 list 共用同一个 modCount
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(0, 2);
list.add("E"); // 修改原 list,modCount 变化
for (String s : sub) { // ❌ 抛出异常
System.out.println(s);
}
✅ 解决方案:
// 方案1:操作 subList 时不要操作原 list
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(0, 2);
sub.add("X"); // 操作 subList 是安全的
// 方案2:创建新的独立列表
List<String> sub = new ArrayList<>(list.subList(0, 2));
4.5 场景五:forEach 中的删除
// ❌ Java 8 forEach 同样会抛异常
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.forEach(s -> {
if ("B".equals(s)) {
list.remove(s); // ❌ 抛出异常
}
});
✅ 正确写法:
// 使用 removeIf(推荐)
list.removeIf(s -> "B".equals(s));
// 或者先收集要删除的元素
List<String> toRemove = new ArrayList<>();
list.forEach(s -> {
if ("B".equals(s)) {
toRemove.add(s);
}
});
list.removeAll(toRemove);
5. 不同集合的行为差异
| 集合类型 | fast-fail | 产生异常的场景 | 说明 |
|---|---|---|---|
| ArrayList | ✅ | 遍历时结构性修改 | 最常用,最易触发 |
| LinkedList | ✅ | 遍历时结构性修改 | 同 ArrayList |
| Vector | ✅ | 遍历时结构性修改 | 古老类,方法同步 |
| HashSet | ✅ | 遍历时结构性修改 | 无序集合 |
| HashMap | ✅ | 遍历时结构性修改 | Key-Value 结构 |
| CopyOnWriteArrayList | ❌ | 不产生 | 迭代器是快照 |
| ConcurrentHashMap | ❌ | 不产生 | 弱一致性迭代器 |
6. Java 8+ 的最佳实践
6.1 使用 removeIf
// 删除所有空字符串
list.removeIf(String::isEmpty);
// 删除满足条件的元素
list.removeIf(s -> s.length() < 3);
6.2 使用 Stream
// 过滤后收集到新列表
List<String> filtered = list.stream()
.filter(s -> !"B".equals(s))
.collect(Collectors.toList());
// 或者修改原列表
list = list.stream()
.filter(s -> !"B".equals(s))
.collect(Collectors.toList());
6.3 使用 List.removeIf 源码
// ArrayList 的 removeIf 实现(Java 8)
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove(); // 内部使用了迭代器的 remove
removed = true;
}
}
return removed;
}
7. 特殊情况的处理
7.1 多线程环境下的安全删除
// 使用并发容器的 removeIf(线程安全)
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.addAll(Arrays.asList("A", "B", "C", "D"));
// CopyOnWriteArrayList 的 removeIf 是线程安全的
list.removeIf(s -> "B".equals(s));
7.2 边遍历边添加元素
// 需求:遍历时在特定位置后添加新元素
// 方案1:使用普通的 for 循环
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (int i = 0; i < list.size(); i++) {
if ("B".equals(list.get(i))) {
list.add(i + 1, "B2"); // 添加后 size 变化,循环继续
i++; // 跳过新加的元素
}
}
// 方案2:创建新列表
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> newList = new ArrayList<>();
for (String s : list) {
newList.add(s);
if ("B".equals(s)) {
newList.add("B2");
}
}
list = newList;
8. 面试高频追问
8.1 fast-fail 和 safe-fail 的区别?
| 特性 | fast-fail | safe-fail |
|---|---|---|
| 检测机制 | 检测 modCount 变化 | 不检测,基于快照 |
| 异常行为 | 立即抛出异常 | 不抛异常,遍历快照 |
| 代表集合 | ArrayList, HashMap | CopyOnWriteArrayList, ConcurrentHashMap |
| 内存开销 | 正常 | 较高(快照) |
| 一致性 | 强一致性 | 弱一致性 |
8.2 modCount 是 volatile 的吗?
不是。modCount 没有 volatile 修饰,这意味着在多线程环境下,一个线程对 modCount 的修改不一定立即对其他线程可见。但 fast-fail 机制的目的是尽早发现问题,不要求绝对准确。
8.3 为什么迭代器的 remove 不会抛异常?
因为迭代器的 remove 方法在删除后会同步更新 expectedModCount:
public void remove() {
// ...
ArrayList.this.remove(lastRet);
expectedModCount = modCount; // 重新同步
// ...
}
8.4 单线程下是否一定不会出现?
不一定!即使在单线程中,使用 subList 或通过其他方式修改集合,也可能触发:
List<String> list = new ArrayList<>();
list.add("A");
List<String> sub = list.subList(0, 1);
list.add("B"); // 修改原集合
sub.iterator().next(); // ❌ 抛出异常
9. 总结对比表
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 遍历删除 | for (item: list) { list.remove(item); } |
iterator.remove() 或 removeIf() |
| 遍历添加 | for (item: list) { list.add(newItem); } |
使用普通 for + 下标,或创建新列表 |
| 多线程遍历 | 多线程直接操作 ArrayList | 使用 CopyOnWriteArrayList |
| subList 使用 | 修改原 list 后遍历 subList | 操作 subList 时不要动原 list |
| forEach 删除 | list.forEach(item -> list.remove(item)) |
list.removeIf() |
一句话总结:
ConcurrentModificationException 是 Java 集合框架的 fast-fail 机制,用于检测迭代过程中的并发修改。其本质是迭代器创建时的
expectedModCount与集合实际的modCount不一致。最佳解决方案是使用迭代器的 remove 方法、removeIf 表达式,或改用并发容器。
📌 记住:增强 for 循环是语法糖,底层还是迭代器;使用迭代器自己的修改方法才是安全的。
如果觉得本文对你有帮助,欢迎点赞、收藏、转发~

|
🌺The End🌺点点关注,收藏不迷路🌺
|
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)