java.util.ConcurrentModificationException 対処法

Java

今日のエントリーも J2SE5.0 時代の古いものです。

しかし、あまり知られていない(目にする機会が少ない)と思われるのでブログ更新用のネタとしました。

今回の問題は二つのスレッドが並行して List オブジェクトにアクセスするよくある問題です。

ここで思い出してください。

Java の Iterator は Fail-Fast に設計されています。

これはイテレーション処理開始後にコレクションの内容が変更されたことを検出したら直ち java.util.ConcurrentModificationException をスローします。

これを回避するには一番手っ取り早いのは synchronized ブロックを使って制御するのが楽です。

synchronizedList() を使ってるから大丈夫だと思っている人は泣いてください。(ヲヒ

Iterator なめたらあかんぜよ!

さて、いったい何を言いたいのか良く解らないと思うので簡潔にまとめていないダラダラとしたサンプルコードをご覧ください。

このプログラムの実行結果は次のような実行時エラーがでます。

堀北真希 Girls_1
北川景子 Girls_1
堀北真希 Girls_2
Exception in thread “main” java.util.concurrent.CompletionException: java.util.ConcurrentModificationException
    at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
    at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1584)
    at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1574)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1689)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.util.ConcurrentModificationException
    at java.util.ArrayList.forEach(ArrayList.java:1252)
    at jp.yucchi.girlfriends_cme.GirlFriends_1.updateGirlFriend(GirlFriends_1.java:27)
    at jp.yucchi.girlfriends_cme.GirlFriends_CME.lambda$main$0(GirlFriends_CME.java:26)
    at jp.yucchi.girlfriends_cme.GirlFriends_CME$$Lambda$1/424058530.get(Unknown Source)
    at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1582)
    … 5 more
Java Result: 1

このプログラムは、GirlFriends_1 スレッドで要素を追加し、イテレーション処理を要素を一つ出力させるごとに一秒間スレッドをスリープさせています。

GirlFriends_2 スレッドでは、要素の削除処理の開始を一秒遅らせています。

GirlFriends_1 スレッドのイテレーション処理中に  GirlFriends_2 スレッド によって List の内容が変更されてしまいます。

二つのスレッドで一つの List に何も考えずに並行処理している無残な結果が悲しいですね。

では、安全に並行処理するために synchronized ブロック を使って終わりではつまらないのでもう一つの解決方法を紹介します。

これを使う機会って滅多にないでしょうけど java.util.concurrent.CopyOnWriteArrayList<E> ってのがあります。

API ドキュメントには次のように書かれています。

基になる配列の新しいコピーを作成することにより、すべての推移的操作(add、setなど)が実装されるArrayListのスレッド・セーフな変数です。
通常、これは非常に効率が悪いのですが、トラバーサル操作が変更を数の点で大幅に上回る場合には、代替手段よりも効率が良い場合があります。
また、これは、トラバーサルを同期できない場合や、同期することを望まないが、並行スレッド間の干渉を排除する必要がある場合に有用です。
「スナップショット」スタイルのイテレータ・メソッドは、イテレータの作成時点での配列状態への参照を使用します。
この配列がイテレータの有効期間中に変更されることは決してないため、干渉は不可能であり、イテレータはConcurrentModificationExceptionをスローしないことが保証されます。
イテレータは、イテレータの作成以降のリストへの追加、削除、または変更を反映しません。
イテレータ自体に対する要素変更操作(remove、setおよびadd)はサポートされません。
これらのメソッドは、UnsupportedOperationExceptionをスローします。
nullを含むすべての要素が許可されます。
メモリー整合性効果: ほかの並行処理コレクションと同様、オブジェクトをCopyOnWriteArrayListに配置する前のスレッド内のアクションは、
別のスレッドでのその要素へのアクセスまたはCopyOnWriteArrayListからの削除に続くアクションよりも前に発生します。

今回のケースではお手軽に使えそうですね。

そう言うことでまたダラダラとコードをご覧ください。と、思ったけど List を CopyOnWriteArrayList に変更するだけなのでやめておきます。

いちおう、 GitHub にアップしておきますので見たい方はどうぞ。

https://github.com/Yucchi-1995/GirlFriends

プログラムの実行結果は次のようになります。

堀北真希 Girls_1
北川景子 Girls_1
堀北真希 Girls_2
桐谷美玲 Girls_1
新垣結衣 Girls_1
剛力彩芽 Girls_1
GirlFriend_1 List
堀北真希
綾瀬はるか
GirlFriend_2 List
堀北真希
綾瀬はるか
GirlFriend List
堀北真希
綾瀬はるか
新垣結衣
剛力彩芽

java.util.ConcurrentModificationException が投げられることなく並行処理が完了しています。

とても簡単です。

API ドキュメントにも書いてありますが CopyOnWriteArrayList はイテレータの作成時点での配列状態への参照を使用し、

この配列がイテレータの有効期間中に変更されることは決してないと言い切っているとおり、

プログラムの実行結果より Girls_1 スレッド、Girls_2 スレッドのイテレーション処理出力がそのとおりに出力されているのが確認できます。

堀北真希 Girls_1
北川景子 Girls_1
堀北真希 Girls_2
桐谷美玲 Girls_1
新垣結衣 Girls_1
剛力彩芽 Girls_1

そして、Girls_1 スレッド、Girls_2 スレッドから返される List の要素は「堀北真希」の一つとなります。

GirlFriend_1 List、GirlFriend_2 List の出力が

GirlFriend_1 List
堀北真希
綾瀬はるか
GirlFriend_2 List
堀北真希
綾瀬はるか

となっているのは main スレッドに戻ってから「綾瀬はるか」が追加され出力されているからです。

そして、最後に「新垣結衣」と「剛力彩芽」を追加して List を表示させています。

CopyOnWriteArrayList って本当に優れものですね!

どうでもいいことですが、このプログラムの欠陥は「北川景子」を削除して、あとで再び List に追加する予定だったのをすっかり忘れてしまったことです。(^_^;)

Hatena タグ: