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 高并发读 + 极少写 读✅✅✅ 写❌

选择策略的核心原则:

  • 单线程优先用 removeAllIterator
  • 并发读多写少 → CopyOnWriteArrayList
  • 避免“边遍历边改结构”的思维定式

所有示例代码已上传至 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-concurrency-3


原始标题:Avoiding the ConcurrentModificationException in Kotlin