1. 引言
在 Kotlin 开发中,ConcurrentModificationException
是一个高频“踩坑”点。它通常出现在你一边遍历集合,一边修改其内容的场景中。虽然名字里带个 “Concurrent”,但别被误导——这个问题不仅发生在多线程环境下,单线程也照常触发。
这种异常一旦抛出,调试起来挺头疼,尤其当你不确定是哪段代码动了集合。本文将深入剖析其成因,并给出几种生产环境验证过的规避方案,帮你彻底绕开这个坑✅。
2. 什么是 ConcurrentModificationException?
简单说:**当一个线程正在遍历集合时,另一个操作(哪怕在同一线程)修改了该集合结构(如增删元素),就会触发 ConcurrentModificationException
**。
来看一个经典反例:
val numbers = mutableListOf(1, 2, 3, 4, 5)
assertThrows<ConcurrentModificationException> {
for (item in numbers) {
if (item == 3) {
numbers.remove(item)
}
}
}
⚠️ 上述代码会直接抛出异常。原因在于:
for-in
循环底层使用迭代器(Iterator)遍历。- 当调用
numbers.remove()
直接修改集合时,会改变集合的modCount
(修改计数)。 - 迭代器在下一次
next()
调用时检测到modCount
不一致,立即抛出异常。
📌 关键点:即使单线程,只要“边遍历边改结构”,就可能触发此异常。并发只是放大了发生概率。
3. 使用 Iterator 安全删除
最直接的解决方案是:使用迭代器自带的 remove()
方法。这是唯一允许在遍历过程中安全删除元素的方式。
val numbers = mutableListOf(1, 2, 3, 4, 5)
val iterator = numbers.iterator()
while (iterator.hasNext()) {
val number = iterator.next()
if (number == 3) {
iterator.remove() // ✅ 安全删除
}
}
✅ 优点:
- 原生支持,无需额外依赖
- 性能好,内存开销小
❌ 缺点:
- 语法略显冗长
- 只适用于删除操作,不能用于添加
💡 小贴士:
iterator.remove()
必须在next()
之后调用,否则会抛IllegalStateException
。
4. 使用 removeAll 实现函数式过滤
Kotlin 标准库提供了更优雅的方案:removeAll
。它接受一个 Lambda 条件,内部安全完成过滤。
val numbers = mutableListOf(1, 2, 3, 4, 5)
numbers.removeAll { it == 3 }
等价写法(可读性更强):
numbers.removeAll { number -> number == 3 }
✅ 优点:
- 函数式风格,简洁清晰
- 内部已处理线程安全逻辑
- 一行代码搞定,不易出错
📌 底层原理:removeAll
会先收集所有匹配元素,再统一执行删除,避免遍历时结构性修改。
5. 修改副本(Copy-and-Swap)
如果你需要更复杂的逻辑判断,可以考虑“修改副本”策略:
var numbers = mutableListOf(1, 2, 3, 4, 5)
val copyNumbers = numbers.toMutableList()
for (number in numbers) {
if (number == 3) {
copyNumbers.remove(number)
}
}
numbers = copyNumbers // 最后替换原引用
✅ 适用场景:
- 需要根据原集合状态做复杂条件判断
- 删除逻辑分散,不适合用
removeAll
⚠️ 注意事项:
- 内存开销大,尤其是大集合
- 需要确保最终赋值原子性(单线程下没问题,多线程需加锁)
- 推荐仅用于小数据量或低频操作
6. 使用 CopyOnWriteArrayList(高并发场景)
对于高频读、低频写的并发场景,推荐使用 CopyOnWriteArrayList
—— 专为这类问题设计的线程安全容器。
val list = CopyOnWriteArrayList(listOf(1, 2, 3, 4, 5))
for (item in list) {
if (item == 3) {
list.remove(item) // ✅ 不会抛异常
}
}
工作机制:
- 每次写操作(add/remove/set)都会创建底层数组的新副本
- 读操作(遍历)始终基于快照进行,无锁
- 写操作完成后,原子更新引用指向新数组
✅ 优点:
- 遍历时修改完全安全
- 读操作无锁,性能极高
❌ 缺点:
- 每次写操作都复制整个数组,写性能差
- 内存占用翻倍(旧数组等待 GC)
- 实时性弱:遍历中看不到最新写入的数据
📌 使用建议:
仅用于 读远多于写 的场景,例如事件监听器列表、配置缓存等。
7. 总结
方案 | 适用场景 | 是否线程安全 | 性能 |
---|---|---|---|
Iterator.remove() |
单线程删除 | ❌ | ✅✅✅ |
removeAll |
条件删除,函数式风格 | 单线程安全 | ✅✅ |
修改副本 | 复杂逻辑,小数据量 | ❌ | ✅/⚠️(看大小) |
CopyOnWriteArrayList |
高并发读 + 极少写 | ✅ | 读✅✅✅ 写❌ |
选择策略的核心原则:
- 单线程优先用
removeAll
或Iterator
- 并发读多写少 →
CopyOnWriteArrayList
- 避免“边遍历边改结构”的思维定式
所有示例代码已上传至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-concurrency-3