Java
 Computer >> コンピューター >  >> プログラミング >> Java

【Java】コレクション反復処理時のConcurrentModificationExceptionを回避する方法

Javaでコレクションオブジェクトを操作している際、あるスレッドが特定のコレクションを反復処理(イテレーション)している最中に、そのコレクションへ要素の追加や削除といった構造的な変更を加えると、ConcurrentModificationException(並行変更例外)がスローされます。

さらに、反復処理の途中でコレクションに要素を追加・削除した後、同じIteratorオブジェクトを使い回して再度内容を反復しようとした場合も、複数のスレッドからコレクションへアクセスしているものと判断され、同様に例外が発生します。

例外が発生する仕組み(フェイルファスト機構)

ArrayListなどの標準コレクションは「フェイルファスト(fail-fast)」と呼ばれる機構を採用しています。内部ではmodCountという変更カウンタを保持しており、イテレータがnext()などを呼び出すたびに、イテレータ生成時のカウンタ値と照合します。不一致が検出されると、即座にConcurrentModificationExceptionをスローして異常を通知します。これはあくまで早期のバグ検出を目的とした仕組みであり、正しい同期処理の代替にはならない点に注意してください。

例外が発生するコード例

import java.util.ArrayList;
import java.util.Iterator;
public class OccurenceOfElements {
    public static void main(String args[]) {
        ArrayList <String> list = new ArrayList<String>();
        //ArrayListオブジェクトの生成
        list.add("JavaFX");
        list.add("Java");
        list.add("WebGL");
        list.add("OpenCV");
        System.out.println("Contents of the array list (first to last): ");
        Iterator<String> it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+", ");
        }
        //list.remove(3);
        list.add(3, "Hadoop");
        while(it.hasNext()) {
            System.out.print(it.next()+", ");
        }
    }
}

実行結果

Contents of the array list (first to last):
JavaFX, Java, WebGL, OpenCV, Exception in thread "main"
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
    at java.util.ArrayList$Itr.next(Unknown Source)
    at sample.OccurenceOfElements.main(OccurenceOfElements.java:23)

この例では、最初のwhileループで反復処理を完了させた後、list.add(3, "Hadoop")によってリストを変更しています。しかし変数itには古いIteratorオブジェクトが保持されたままのため、要素追加によってhasNext()が再びtrueを返し、next()を呼び出した時点でcheckForComodification()により変更が検出され、例外がスローされます。

解決策

この問題を回避するには、状況に応じて以下の対策を講じます。

  • マルチスレッド環境でアクセスする場合:synchronizedブロックやsynchronizedメソッドでコレクションへのアクセスを同期化する。あるいはCollections.synchronizedList()でラップする。
  • データ取得中にコレクションを変更する場合:変更後に必ずlist.iterator()を呼び出して、Iteratorオブジェクトを再取得してから反復処理を行う。
  • 反復中に要素を削除したい場合:コレクション側のremove()ではなく、Iterator自身のremove()メソッドを使用する。
  • 高い並行性が必要な場合:CopyOnWriteArrayListやConcurrentHashMapなど、java.util.concurrentパッケージのスレッドセーフなコレクションを利用する。

修正後のコード例

import java.util.ArrayList;
import java.util.Iterator;
public class OccurenceOfElements {
    public static void main(String args[]) {
        ArrayList <String> list = new ArrayList<String>();
        //ArrayListオブジェクトの生成
        list.add("JavaFX");
        list.add("Java");
        list.add("WebGL");
        list.add("OpenCV");
        System.out.println("Contents of the array list (first to last): ");
        Iterator<String> it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+". ");
        }
        list.remove(3);
        System.out.println("");
        System.out.println("Contents of the array list after removal: ");
        it = list.iterator(); //変更後にIteratorを再取得
        while(it.hasNext()) {
            System.out.print(it.next()+". ");
        }
    }
}

実行結果

Contents of the array list (first to last):
JavaFX. Java. WebGL. OpenCV.
Contents of the array list after removal:
JavaFX. Java. WebGL.

このように、リストを変更した後にlist.iterator()を呼び出して新しいIteratorを取得し直すことで、ConcurrentModificationExceptionを発生させることなく安全に反復処理を行えます。マルチスレッド環境では、加えて適切な同期化やスレッドセーフなコレクションの活用を組み合わせることで、より堅牢なプログラムを実現できます。

  1. 【Java】CollectionインターフェースとCollectionsクラスの違いとは?違いを徹底解説

    はじめにJavaのコレクションフレームワークを学んでいると、「Collection」と「Collections」という名前が似ている2つの存在に出会います。名前はほぼ同じですが、その役割はまったく異なります。この記事では、両者の違いをわかりやすく解説します。CollectionとCollectionsの主な違いCollectionはJavaにおけるインターフェースです。一方、Collectionsはユーティリティクラスであり、コレクションを操作するための便利な静的メソッドを提供します。Set、List、QueueなどはCollectionインターフェースのサブインターフェースです。また、Map

  2. Javaでデッドロックを回避するには?発生原因と対策を実例付きで解説

    Javaにおけるデッドロック(deadlock)とは、2つ以上のスレッドが互いにロックを待ち合い、永久にブロックされた状態が続くプログラミング上の問題です。デッドロックは少なくとも2つのスレッドと2つ以上のリソースが存在する場合に発生します。デッドロックが起きると、スレッドは互いに相手が保持しているロックの解放を待ち続けるため、プログラム全体が停止してしまいます。本記事では、デッドロックの回避方法と、実際のコード例を通じてその仕組みを解説します。デッドロックを回避するための3つの対策1. ネストされたロック(入れ子のロック)を避けるデッドロックの主な原因は、複数のスレッドに対してロックを与える