Kotlin多线程访问集合求助:并发修改异常问题
嘿,我来帮你搞定这个问题~你遇到的ConcurrentModificationException,核心原因其实是对@Synchronized的理解有点偏差:给myList的getter加注解,只是保证获取集合实例这个动作是线程安全的,但当你拿到集合之后,forEach遍历和remove修改这两个操作是在各自线程里独立执行的,完全没有同步约束——这就导致遍历到一半集合被修改,直接触发了异常。
下面给你三种靠谱的解决方式,你可以根据自己的场景选择:
1. 使用线程安全的集合类(读多写少场景首选)
Java并发包提供的CopyOnWriteArrayList天生就是线程安全的,遍历的时候会基于集合的快照进行,完全不会因为并发修改抛出异常。适合读操作远多于写操作的场景(比如你的示例里遍历是主要操作,删除是偶尔行为):
import java.util.concurrent.CopyOnWriteArrayList class ListTest() { val myList = CopyOnWriteArrayList<String>().apply { repeat(10000) { add("stuff: $it") } } } fun main() = runBlocking<Unit> { val listTest = ListTest() launch(Dispatchers.Default) { delay(1L) listTest.myList.remove("stuff: 54") } launch { listTest.myList.forEach { println(it) } } }
注意:这个类每次修改都会复制底层数组,所以写操作的开销比较大,读多写少的时候用它最划算。
2. 手动同步所有集合操作(通用场景)
把对集合的所有操作(遍历、修改)都封装成同步方法,用同一个锁对象保证同一时间只有一个线程能操作集合:
class ListTest() { private val myList = mutableListOf<String>() init { repeat(10000) { myList.add("stuff: $it") } } // 同步遍历操作 fun iterateList(action: (String) -> Unit) { synchronized(this) { myList.forEach(action) } } // 同步删除操作 fun removeElement(element: String) { synchronized(this) { myList.remove(element) } } } fun main() = runBlocking<Unit> { val listTest = ListTest() launch(Dispatchers.Default) { delay(1L) listTest.removeElement("stuff: 54") } launch { listTest.iterateList { println(it) } } }
这里用this作为锁对象,也可以专门创建一个private val lock = Any()来做锁,避免和其他可能的同步操作冲突。
3. 用协程Mutex(Kotlin协程场景首选)
既然你用了runBlocking和协程,那用Kotlin协程专用的Mutex会更合适——它不会阻塞线程,只会挂起协程,性能更优:
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class ListTest() { private val myList = mutableListOf<String>() private val mutex = Mutex() // 协程互斥锁 init { repeat(10000) { myList.add("stuff: $it") } } suspend fun iterateList(action: (String) -> Unit) { mutex.withLock { myList.forEach(action) } } suspend fun removeElement(element: String) { mutex.withLock { myList.remove(element) } } } fun main() = runBlocking<Unit> { val listTest = ListTest() launch(Dispatchers.Default) { delay(1L) listTest.removeElement("stuff: 54") } launch { listTest.iterateList { println(it) } } }
withLock会自动帮你加锁和释放锁,不用手动处理,非常安全。
总结一下:不要只同步集合的getter,要同步整个操作过程;协程环境优先用Mutex,普通线程环境用synchronized块,读多写少用CopyOnWriteArrayList。
内容的提问来源于stack exchange,提问作者MVP

