java.util.ConcurrentModificationException

java.util.ConcurrentModificationExceptionは、Javaのコレクションクラスを使用する際に非常に一般的な例外です。Javaのコレクションクラスはfail-fastです。つまり、イテレータを使用している間にコレクションが変更されると、iterator.next()ConcurrentModificationExceptionをスローします。Concurrent modification exceptionは、マルチスレッド環境だけでなく、シングルスレッドのJavaプログラミング環境でも発生する可能性があります。

java.util.ConcurrentModificationException

以下は、例を使用したconcurrent modification exceptionのシナリオです。

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class ConcurrentModificationExceptionExample {

	public static void main(String args[]) {
		List myList = new ArrayList();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		Iterator it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3"))
				myList.remove(value);
		}

		Map myMap = new HashMap();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("2")) {
				myMap.put("1", "4");
				// myMap.put("4", "4");
			}
		}

	}
}

上記のプログラムは、実行時にjava.util.ConcurrentModificationExceptionをスローします。以下は、コンソールログに表示される内容です。

List Value:1
List Value:2
List Value:3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)

出力されたスタックトレースから明らかなように、同時変更例外はiteratorのnext()関数を呼び出す時にスローされます。Iteratorが変更をチェックする方法について疑問がある場合、その実装はAbstractListクラスに存在します。そこで、int型の変数modCountが定義されています。modCountはリストのサイズが変更された回数を提供します。modCountの値は、すべてのnext()呼び出しで使用され、関数checkForComodification()で変更をチェックします。今度はリストの部分をコメントアウトしてプログラムを再実行してみてください。すると、ConcurrentModificationExceptionがスローされなくなることがわかります。出力:

Map Value:3
Map Value:2
Map Value:4

myMapの既存のキー値を更新しているため、そのサイズは変更されていないため、ConcurrentModificationExceptionが発生しません。出力は、HashMapのキーセットがリストのように順序付けられていないため、システムによって異なる場合があります。HashMapに新しいキー値を追加するステートメントのコメントを解除すると、ConcurrentModificationExceptionが発生します。

マルチスレッド環境でConcurrentModificationExceptionを回避する方法

  1. リストを配列に変換してから、配列上で反復処理を行うことができます。このアプローチは、小〜中規模のリストに対してはうまく機能しますが、リストが大きい場合はパフォーマンスに影響します。
  2. リストを反復処理する際には、それを同期化ブロック内に配置することでロックできます。この方法は、マルチスレッディングの利点を失うため、推奨されません。
  3. もしJDK1.5以上を使用している場合は、ConcurrentHashMapCopyOnWriteArrayListクラスを使用できます。これは、ConcurrentModificationExceptionを回避するための推奨される方法です。

単一スレッド環境でのConcurrentModificationExceptionを回避するために

反復子remove()関数を使用して、基礎となるコレクションオブジェクトからオブジェクトを削除できます。しかし、この場合、同じオブジェクトを削除し、リストから他のオブジェクトを削除することはできません。Concurrent Collection クラスを使用した例を実行しましょう。

package com.journaldev.ConcurrentModificationException;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class AvoidConcurrentModificationException {

	public static void main(String[] args) {

		List<String> myList = new CopyOnWriteArrayList<String>();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		Iterator<String> it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3")) {
				myList.remove("4");
				myList.add("6");
				myList.add("7");
			}
		}
		System.out.println("List Size:" + myList.size());

		Map<String, String> myMap = new ConcurrentHashMap<String, String>();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator<String> it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("1")) {
				myMap.remove("3");
				myMap.put("4", "4");
				myMap.put("5", "5");
			}
		}

		System.out.println("Map Size:" + myMap.size());
	}

}

上記のプログラムの出力は以下の通りです。プログラムからConcurrentModificationExceptionがスローされていないことがわかります。

List Value:1
List Value:2
List Value:3
List Value:4
List Value:5
List Size:6
Map Value:1
Map Value:2
Map Value:4
Map Value:5
Map Size:4

上記の例から明らかなように:

  1. Concurrent Collection クラスは安全に変更でき、ConcurrentModificationExceptionはスローされません。

  2. CopyOnWriteArrayListの場合、反復子はリストの変更を受け入れず、元のリストで動作します。

  3. ConcurrentHashMapの場合、動作は常に同じではありません。条件について:

    if(key.equals("1")){
    	myMap.remove("3");}
    

    出力は:

    Map Value:1
    Map Value:null
    Map Value:4
    Map Value:2
    Map Size:4
    

    「4」というキーで追加された新しいオブジェクトを受け入れますが、「5」というキーで次に追加されたオブジェクトは受け入れません。これを以下のように変更すると:

    if(key.equals("3")){
    	myMap.remove("2");}
    

    出力は:

    Map Value:1
    Map Value:3
    Map Value:null
    Map Size:4
    

    この場合、新しく追加されたオブジェクトは考慮されていません。ConcurrentHashMapを使用している場合は、新しいオブジェクトの追加を避けるべきです。同じプログラムが異なる値を出力する可能性があることに注意してください。なぜなら、HashMapのkeysetは順序付けられていないからです。

Java.util.ConcurrentModificationExceptionを回避するためにforループを使用します

シングルスレッド環境で作業しており、コードがリストに追加された追加オブジェクトを処理することを希望する場合は、Iteratorではなくforループを使用して行うことができます。

for(int i = 0; i<myList.size(); i++){
	System.out.println(myList.get(i));
	if(myList.get(i).equals("3")){
		myList.remove(i);
		i--;
		myList.add("6");
	}
}

同じオブジェクトを削除しているため、カウンターを減らしていますが、次のオブジェクトまたはさらに遠いオブジェクトを削除する必要がある場合は、カウンターを減らす必要はありません。自分で試してみてください。 🙂 もうひとつ: subListを使用して元のリストの構造を変更しようとすると、ConcurrentModificationExceptionが発生します。簡単な例でこれを見てみましょう。

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExceptionWithArrayListSubList {

	public static void main(String[] args) {

		List names = new ArrayList<>();
		names.add("Java");
		names.add("PHP");
		names.add("SQL");
		names.add("Angular 2");

		List first2Names = names.subList(0, 2);

		System.out.println(names + " , " + first2Names);

		names.set(1, "JavaScript");
		// 下記の出力を確認してください。 :)
		System.out.println(names + " , " + first2Names);

		// リストのサイズを変更してConcurrentModificationExceptionを取得します
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // this line throws exception

	}

}

上記プログラムの出力は次のとおりです:

[Java, PHP, SQL, Angular 2] , [Java, PHP]
[Java, JavaScript, SQL, Angular 2] , [Java, JavaScript]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1282)
	at java.base/java.util.ArrayList$SubList.listIterator(ArrayList.java:1151)
	at java.base/java.util.AbstractList.listIterator(AbstractList.java:311)
	at java.base/java.util.ArrayList$SubList.iterator(ArrayList.java:1147)
	at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:465)
	at java.base/java.lang.String.valueOf(String.java:2801)
	at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionWithArrayListSubList.main(ConcurrentModificationExceptionWithArrayListSubList.java:26)

ArrayList subListのドキュメントによれば、構造変更はsubListメソッドによって返されたリストでのみ許可されます。返されたリストのすべてのメソッドは、バッキングリストの実際のmodCountがその期待値と等しいかどうかを最初にチェックし、等しくない場合はConcurrentModificationExceptionをスローします。

すべてのサンプルコードは、私たちのGitHubリポジトリからダウンロードできます。

Source:
https://www.digitalocean.com/community/tutorials/java-util-concurrentmodificationexception