[Java] 例外:ConcurrentModificationExceptionエラーの原因や対処法を解説

ConcurrentModificationExceptionは、コレクション(例:ArrayListやHashMap)を反復処理中に、そのコレクションが同時に変更された場合にスローされる例外です。

例えば、Iteratorを使ってリストを走査中に、remove()add()などでリストを変更すると発生します。

対処法としては、Iteratorremove()メソッドを使用して要素を削除するか、CopyOnWriteArrayListなどのスレッドセーフなコレクションを使用することで回避できます。

また、for-eachループではなく明示的にIteratorを使うことも有効です。

この記事でわかること
  • ConcurrentModificationExceptionの原因
  • 具体的な発生例の紹介
  • 例外の対処法の解説
  • スレッドセーフなコレクションの活用法
  • 安全なコレクション操作の方法

目次から探す

ConcurrentModificationExceptionとは

ConcurrentModificationExceptionは、Javaプログラミングにおいて、コレクションをイテレートしている最中に、そのコレクションが変更された場合に発生する例外です。

この例外は、主にIteratorを使用してコレクションを走査している際に、他のスレッドや同じスレッド内でコレクションの要素を追加、削除、または変更した場合にトリガーされます。

これにより、イテレーションの整合性が損なわれ、予期しない動作やエラーを引き起こす可能性があります。

特にマルチスレッド環境では、この例外に注意が必要です。

ConcurrentModificationExceptionの原因

コレクションの変更とイテレーションの関係

コレクションをイテレートしている最中に、そのコレクションに対して変更が加えられると、ConcurrentModificationExceptionが発生します。

イテレーションは、コレクションの要素を順に処理するプロセスですが、同時に要素を追加または削除すると、イテレーションの状態が不整合になり、例外がスローされます。

このため、コレクションの変更は、イテレーションが完了するまで行わないことが推奨されます。

Iteratorとfor-eachループの違い

Iteratorを使用する場合、明示的に要素を取得し、削除することができます。

一方、for-eachループは、内部的にIteratorを使用していますが、要素の削除や追加を行うと、ConcurrentModificationExceptionが発生します。

for-eachループは簡潔で便利ですが、コレクションの変更には注意が必要です。

マルチスレッド環境での問題

マルチスレッド環境では、複数のスレッドが同時に同じコレクションにアクセスし、変更を加えることがあります。

この場合、あるスレッドがコレクションをイテレートしている最中に、別のスレッドがそのコレクションを変更すると、ConcurrentModificationExceptionが発生します。

スレッド間の競合を避けるためには、適切な同期機構を使用することが重要です。

コレクションの内部構造の変更

コレクションの内部構造が変更されると、イテレーションの整合性が損なわれることがあります。

例えば、ArrayListのサイズが変更されると、内部的な配列の参照が無効になる可能性があります。

このような変更がイテレーション中に発生すると、ConcurrentModificationExceptionがスローされます。

コレクションの変更は、イテレーションが完了してから行うことが望ましいです。

ConcurrentModificationExceptionの具体例

ArrayListでの発生例

ArrayListを使用している場合、イテレーション中に要素を追加または削除すると、ConcurrentModificationExceptionが発生します。

以下のサンプルコードでは、ArrayListをイテレートしている最中に要素を削除しようとしています。

import java.util.ArrayList;
import java.util.List;
public class App {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        for (String item : list) {
            System.out.println(item);
            // イテレーション中に要素を削除
            list.remove(item); // ここで例外が発生
        }
    }
}
A
Exception in thread "main" java.util.ConcurrentModificationException
        at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
        at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
        at App.main(App.java:10)

HashMapでの発生例

HashMapでも同様に、イテレーション中に要素を変更するとConcurrentModificationExceptionが発生します。

以下のサンプルコードでは、HashMapをイテレート中に要素を追加しています。

import java.util.HashMap;
import java.util.Map;
public class App {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("1", "A");
        map.put("2", "B");
        map.put("3", "C");
        for (String key : map.keySet()) {
            System.out.println(key + ": " + map.get(key));
            // イテレーション中に要素を追加
            map.put("4", "D"); // ここで例外が発生
        }
    }
}
1: A
Exception in thread "main" java.util.ConcurrentModificationException
        at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1606)
        at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1629)
        at App.main(App.java:10)
 

for-eachループでの発生例

for-eachループを使用している場合も、コレクションの変更によってConcurrentModificationExceptionが発生します。

以下のサンプルコードでは、for-eachループ中に要素を削除しています。

import java.util.ArrayList;
import java.util.List;
public class App {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        for (String item : list) {
            System.out.println(item);
            // イテレーション中に要素を削除
            list.remove(item); // ここで例外が発生
        }
    }
}
A
Exception in thread "main" java.util.ConcurrentModificationException
        at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
        at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
        at App.main(App.java:10)

Iteratorを使った場合の発生例

Iteratorを使用することで、要素を安全に削除することができますが、イテレーション中にコレクションを変更すると、ConcurrentModificationExceptionが発生することがあります。

以下のサンプルコードでは、Iteratorを使って要素を削除していますが、コレクションを変更したために例外が発生します。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class App {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            System.out.println(item);
            // イテレーション中にコレクションを変更
            list.add("D"); // ここで例外が発生
        }
    }
}
A
Exception in thread "main" java.util.ConcurrentModificationException
        at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
        at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
        at App.main(App.java:13)

ConcurrentModificationExceptionの対処法

Iteratorのremove()メソッドを使う

Iteratorを使用する際には、remove()メソッドを利用することで、イテレーション中に安全に要素を削除できます。

このメソッドは、現在のイテレーションの要素を削除するため、ConcurrentModificationExceptionを回避できます。

以下のサンプルコードでは、Iteratorを使って要素を削除しています。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class App {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String item = iterator.next();
            System.out.println(item);
            // Iteratorのremove()メソッドを使用して要素を削除
            if (item.equals("B")) {
                iterator.remove(); // 安全に削除
            }
        }
        System.out.println("残りの要素: " + list);
    }
}
A
B
C
残りの要素: [A, C]

Concurrentなコレクションを使う

Javaには、スレッドセーフなコレクションが用意されています。

これらのコレクションは、複数のスレッドからの同時アクセスを安全に処理するために設計されています。

例えば、ConcurrentHashMapCopyOnWriteArrayListなどがあります。

これらを使用することで、ConcurrentModificationExceptionを回避できます。

CopyOnWriteArrayListの使用

CopyOnWriteArrayListは、要素の変更が行われるたびに内部の配列をコピーするため、イテレーション中に要素を安全に追加または削除できます。

以下のサンプルコードでは、CopyOnWriteArrayListを使用して要素を追加しています。

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class App {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        for (String item : list) {
            System.out.println(item);
            // イテレーション中に要素を追加
            list.add("D"); // 例外は発生しない
        }
        System.out.println("最終的な要素: " + list);
    }
}
A
B
C
最終的な要素: [A, B, C, D, D, D]

スレッドセーフなコレクションの選択

スレッドセーフなコレクションを選択することは、マルチスレッド環境での安全な操作において重要です。

Collections.synchronizedList()Collections.synchronizedMap()を使用することで、通常のコレクションをスレッドセーフにすることができます。

以下のサンプルコードでは、synchronizedListを使用しています。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class App {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        list.add("A");
        list.add("B");
        list.add("C");
        synchronized (list) { // 明示的な同期
            for (String item : list) {
                System.out.println(item);
                // 要素を削除する場合も同期が必要
                if (item.equals("B")) {
                    list.remove(item); // 安全に削除
                }
            }
        }
        System.out.println("残りの要素: " + list);
    }
}
A
B
C
残りの要素: [A, C]

Stream APIを使った安全な操作

Java 8以降では、Stream APIを使用することで、コレクションの要素を安全に操作できます。

Streamを使用すると、コレクションを変更することなく、フィルタリングやマッピングを行うことができます。

以下のサンプルコードでは、Streamを使用して要素をフィルタリングしています。

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class App {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        list.add("C");
        // Streamを使用して要素をフィルタリング
        List<String> filteredList = list.stream()
                .filter(item -> !item.equals("B")) // "B"を除外
                .collect(Collectors.toList());
        System.out.println("フィルタリング後の要素: " + filteredList);
    }
}
フィルタリング後の要素: [A, C]

応用例:スレッドセーフなコレクションの活用

ConcurrentHashMapの使用例

ConcurrentHashMapは、スレッドセーフなマップの実装であり、複数のスレッドからの同時アクセスを効率的に処理できます。

以下のサンプルコードでは、ConcurrentHashMapを使用して、複数のスレッドが同時にマップに要素を追加しています。

import java.util.concurrent.ConcurrentHashMap;
public class App {
    public static void main(String[] args) {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        // スレッドを作成して要素を追加
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                map.put("Key" + i, "Value" + i);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 6; i <= 10; i++) {
                map.put("Key" + i, "Value" + i);
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最終的なマップ: " + map);
    }
}
最終的なマップ: {Key1=Value1, Key2=Value2, Key3=Value3, Key4=Value4, Key5=Value5, Key6=Value6, Key7=Value7, Key8=Value8, Key9=Value9, Key10=Value10}

CopyOnWriteArrayListの使用例

CopyOnWriteArrayListは、要素の変更が行われるたびに内部の配列をコピーするため、イテレーション中に要素を安全に追加または削除できます。

以下のサンプルコードでは、CopyOnWriteArrayListを使用して、スレッドが同時に要素を追加しています。

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class App {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        // スレッドを作成して要素を追加
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                list.add("Item" + i);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 6; i <= 10; i++) {
                list.add("Item" + i);
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最終的なリスト: " + list);
    }
}
最終的なリスト: [Item1, Item2, Item3, Item4, Item5, Item6, Item7, Item8, Item9, Item10]

Collections.synchronizedListの使用例

Collections.synchronizedList()を使用することで、通常のリストをスレッドセーフにすることができます。

以下のサンプルコードでは、synchronizedListを使用して、複数のスレッドが同時にリストに要素を追加しています。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class App {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        // スレッドを作成して要素を追加
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                list.add("Element" + i);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 6; i <= 10; i++) {
                list.add("Element" + i);
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 明示的に同期を取る
        synchronized (list) {
            System.out.println("最終的なリスト: " + list);
        }
    }
}
最終的なリスト: [Element1, Element2, Element3, Element4, Element5, Element6, Element7, Element8, Element9, Element10]

よくある質問

ConcurrentModificationExceptionはなぜ発生するのですか?

ConcurrentModificationExceptionは、コレクションをイテレートしている最中に、そのコレクションが変更された場合に発生します。

具体的には、Iteratorfor-eachループを使用している際に、他のスレッドまたは同じスレッド内で要素の追加、削除、または変更が行われると、イテレーションの整合性が損なわれ、例外がスローされます。

この例外は、コレクションの状態が不整合になることを防ぐために設計されています。

Iteratorを使わずに例外を回避する方法はありますか?

Iteratorを使わずに例外を回避する方法としては、以下のような手段があります。

  • CopyOnWriteArrayListを使用する:このコレクションは、要素の変更が行われるたびに内部の配列をコピーするため、イテレーション中に要素を安全に追加または削除できます。
  • Stream APIを使用する:Streamを利用してコレクションをフィルタリングやマッピングすることで、元のコレクションを変更せずに操作が可能です。
  • 新しいリストを作成する:イテレーション中に変更が必要な場合は、元のコレクションを変更せずに新しいリストを作成し、必要な要素を追加する方法もあります。

マルチスレッド環境での安全なコレクション操作方法は?

マルチスレッド環境での安全なコレクション操作には、以下の方法があります。

  • Concurrentなコレクションを使用するConcurrentHashMapCopyOnWriteArrayListなど、スレッドセーフなコレクションを使用することで、同時アクセスを安全に処理できます。
  • Collections.synchronizedList()を使用する:通常のリストをスレッドセーフにするために、Collections.synchronizedList()を使用し、明示的に同期を取ることが重要です。
  • 適切な同期機構を使用するsynchronizedブロックやReentrantLockなどを使用して、コレクションへのアクセスを制御し、競合状態を避けることができます。

まとめ

この記事では、JavaにおけるConcurrentModificationExceptionの原因や具体例、対処法について詳しく解説しました。

特に、コレクションをイテレートしている最中に変更が加えられることが、どのようにしてこの例外を引き起こすのかを理解することが重要です。

スレッドセーフなコレクションや適切な同期機構を利用することで、マルチスレッド環境でも安全にコレクションを操作できるようになりますので、ぜひ実際のプログラムに取り入れてみてください。

  • URLをコピーしました!
目次から探す