Kotlin中的"并发修改刺客":ConcurrentModificationException全解析

124 阅读3分钟

Kotlin中的"并发修改刺客":ConcurrentModificationException全解析

当你在Kotlin中愉快地遍历集合时,突然弹出的ConcurrentModificationException就像程序里的"刺客",冷不丁给你一刀。这个让人又爱又恨的异常,今天我们就来揭开它的神秘面纱。

一、刺客的真面目:什么是ConcurrentModificationException?

这个异常是Java集合框架的"安全卫士",当它检测到集合在迭代过程中被意外修改时,就会果断抛出异常。用Kotlin的话说就是:"嘿,兄弟,你在遍历我时别乱动我!"^[1]^

典型犯罪现场

val list = mutableListOf("A", "B", "C")
for (item in list) {
    if (item == "B") {
        list.remove(item) // 刺客现身!
    }
}

二、刺客的作案手法:底层机制揭秘

1. Fast-Fail机制:集合的"心跳检测"

每个集合都有一个modCount计数器,就像心跳监测仪:

  • 每次结构性修改(add/remove)时,心跳+1
  • 迭代器创建时会记录当前心跳值到expectedModCount
  • 每次迭代时检查:如果心跳不对,立即报警^[1][6]^

Kotlin源码片段

// ArrayList的迭代器实现(简化版)
final fun checkForComodification() {
    if (modCount != expectedModCount)
        throw ConcurrentModificationException()
}

2. 单线程 vs 多线程场景

  • 单线程:就像你边数钱边往钱包里塞钱,自己把自己搞晕了
  • 多线程:一个线程数钱,另一个线程偷偷抽钱,结果当然会乱套^[3]^

三、防御策略:如何驯服这个刺客?

1. 单线程场景:优雅的删除姿势

方案A:使用迭代器的remove()

val list = mutableListOf("A", "B", "C")
val iterator = list.iterator()
while (iterator.hasNext()) {
    if (iterator.next() == "B") {
        iterator.remove() // 安全删除
    }
}

方案B:Kotlin特色解法

// 使用removeIf(Java 8+风格)
list.removeIf { it == "B" }

// 或者先收集后删除
val toRemove = list.filter { it == "B" }
list.removeAll(toRemove)

2. 多线程场景:给刺客戴上"镣铐"

方案A:同步锁

val lock = Any()
synchronized(lock) {
    list.forEach {
        if (it == "B") {
            list.remove(it) // 在同步块中操作
        }
    }
}

方案B:并发集合(推荐)

// 使用CopyOnWriteArrayList
val safeList = CopyOnWriteArrayList(list)
safeList.removeIf { it == "B" } // 线程安全

// 或者ConcurrentHashMap
val map = ConcurrentHashMap<String, Any>()
map.keys.removeIf { it == "keyToRemove" }

3. 终极防御:设计模式

方案A:防御性复制

for (item in List(list)) { // 创建新列表
    if (item == "B") {
        list.remove(item) // 原列表修改不影响遍历
    }
}

方案B:函数式编程

// 使用filter创建新集合
list = list.filter { it != "B" }.toMutableList()

四、实战案例:Android开发中的刺客

在RecyclerView的适配器中,经常遇到这样的"刺客":

// 错误示范
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    val item = dataList[position]
    if (item.isExpired) {
        dataList.remove(item) // 刺客出没!
        notifyItemRemoved(position)
    }
}

正确做法

// 方案1:先标记后处理
private fun removeExpiredItems() {
    val expired = dataList.filter { it.isExpired }
    dataList.removeAll(expired)
    notifyDataSetChanged()
}

// 方案2:使用DiffUtil(推荐)
val diffResult = DiffUtil.calculateDiff(MyDiffCallback(oldList, newList))
diffResult.dispatchUpdatesTo(this)

五、幽默小贴士:如何与刺客和平共处

  1. 刺客守则第一条:迭代时别乱动集合,就像约会时别总看手机
  2. 刺客守则第二条:多线程环境用并发集合,就像开车要系安全带
  3. 刺客守则第三条:Kotlin的removeIf是秘密武器,比Java更优雅^[10]^
  4. 刺客终极奥义:考虑是否真的需要修改集合,有时候"只读不改"更安全

结语:与刺客共舞的艺术

ConcurrentModificationException这个"刺客",其实是集合框架给我们的善意提醒。通过理解它的工作原理,掌握正确的防御技巧,我们不仅能写出更健壮的代码,还能在调试时少掉几根头发。记住:在Kotlin的世界里,与集合共舞的最高境界,是让修改变得优雅而安全。^[5]^

下次当你在Kotlin中遇到这个刺客时,不妨会心一笑:"嘿,老朋友,又来检查我的代码了?"然后从容地拿出我们今天学到的防御招式,让它知难而退。