Kotlin で Android アプリの実装中に、ConcurrentModificationException が発生したので記事にしておきます。
発生条件
下記は、Exception が発生した状態を、簡易的にテストで表したものです。
@Test
fun test() {
val map = mutableMapOf(1 to "One", 2 to "Two", 3 to "Three")
thread(name = "thread-a") {
map.keys.forEach { _ ->
// key を使った処理を実行
Thread.sleep(100)
}
}
thread(name = "thread-b") {
// map の更新
map.remove(1)
}
Thread.sleep(1000)
}
一方のスレッド (thread-a) では Map の keys.forEach を使った処理を行っており、もう一方のスレッド (thread-b) では Map を変更するような処理を行っています。
これを実行すると、下記の Exception が発生します。
Exception in thread "thread-a" java.util.ConcurrentModificationException
at java.base/java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:719)
at java.base/java.util.LinkedHashMap$LinkedKeyIterator.next(LinkedHashMap.java:741)
at com.ykato.designpatterns.ExampleUnitTest$test$1.invoke(ExampleUnitTest.kt:34)
at com.ykato.designpatterns.ExampleUnitTest$test$1.invoke(ExampleUnitTest.kt:14)
at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)
原因
ConcurrentModificationException は、あるスレッドが Collection で反復処理を行っている間に、別のスレッドがそのCollectionを変更すると発生します。
keys のドキュメンテーションコメントには特に記載がありませんが、keys の forEach も Map 自体の forEach と同様(Collection の反復処理)の扱いとなるようです。
Map の forEach には気を遣っていましたが、keys の forEach はノーマークでした。。。
/**
* Returns a [MutableSet] of all keys in this map.
*/
override val keys: MutableSet<K>
回避策
この問題は map.keys
を map.keys.toSet
に変える(コピーする)ことで回避できます。