java.util.ConcurrentModificationExceptionを日本語で自然に言い換えてください。1つの選択肢で構いません。→ java.util.ConcurrentModificationExceptionは、複数のスレッドから同時に修正が行われた場合に発生する例外です。
Javaコレクションクラスを使用している際の非常に一般的な例外は、java.util.ConcurrentModificationExceptionです。Javaのコレクションクラスは、フェイルファスト(Fail-Fast)なので、イテレータを使用してスレッドがトラバースしている間にコレクションが変更される場合、iterator.next()がConcurrentModificationExceptionをスローします。Concurrent modification exceptionは、マルチスレッド環境だけでなく、シングルスレッドのJavaプログラミング環境でも発生することがあります。
java.util.ConcurrentModificationExceptionを日本語で言い換えると、「java.util.ConcurrentModificationException」となります。
package com.scdev.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<String> myList = new ArrayList<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(value);
}
Map<String, String> myMap = new HashMap<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("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.scdev.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)
出力のスタックトレースから明らかなように、同時変更例外はiteratorのnext()関数を呼び出したときに発生します。変更がどのように検出されるか詳しく知りたい場合、その実装はAbstractListクラスにあります。このクラスではint型の変数modCountが定義されています。このmodCountはリストのサイズが変更された回数を示します。modCountの値はnext()の呼び出しの度に使用され、関数checkForComodification()内で変更があるかどうかを確認します。今度はリストの部分をコメントアウトしてプログラムを再実行してみてください。同時変更例外はもはやスローされないことがわかるでしょう。出力:
Map Value:3
Map Value:2
Map Value:4
既存のmyMapのキーバリューを更新しているため、そのサイズは変わらず、ConcurrentModificationExceptionは発生しません。出力結果はシステムによって異なる可能性があります。HashMapのキーセットはリストのように順序付けされていませんので、システムによって出力は異なるかもしれません。HashMapに新しいキーバリューを追加するコードをコメントアウトを解除すると、ConcurrentModificationExceptionが発生します。
マルチスレッド環境でConcurrentModificationExceptionを回避するために
-
- リストを配列に変換してから、配列で反復処理することができます。この方法は、リストが小さか中程度の大きさの場合には効果的ですが、リストが大きい場合はパフォーマンスに大きく影響します。
-
- 反復処理中にリストをロックすることで、同期化されたブロック内に配置することができます。ただし、このアプローチはマルチスレッドの利点を失うため、推奨されていません。
- JDK1.5以降を使用している場合は、ConcurrentHashMapとCopyOnWriteArrayListクラスを使用することができます。これは、同時変更例外を回避するための推奨される方法です。
単一スレッド環境でConcurrentModificationExceptionを回避するために
コレクションのイテレータ remove() 関数を使用して、基礎となるコレクションオブジェクトからオブジェクトを削除することができます。ただし、この場合、リストから同じオブジェクトを削除することができますが、他のオブジェクトは削除できません。Concurrent Collection クラスを使用して例を実行しましょう。
package com.scdev.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
上記の例から明らかなように、
-
- ConcurrentCollectionのクラスは安全に修正することができ、ConcurrentModificationExceptionをスローしません。
CopyOnWriteArrayListの場合、イテレータはリストの変更を受け入れず、元のリスト上で動作します。
ConcurrentHashMapの場合、動作は常に同じではありません。条件において、
if(key.equals(“1”)){
myMap.remove(“3”);}
ならば、出力は次のようになります:
Mapの値:1
Mapの値:null
Mapの値:4
Mapの値:2
Mapのサイズ:4
この場合、キー「4」と一緒に追加された新しいオブジェクトが取得されますが、キー「5」と一緒に追加された次のオブジェクトは取得されません。次に条件を以下のように変更すると、
if(key.equals(“3”)){
myMap.remove(“2”);}
ならば、出力は次のようになります:
Mapの値:1
Mapの値:3
Mapの値:null
Mapのサイズ:4
この場合、新しく追加されたオブジェクトは考慮されません。ConcurrentHashMapを使用している場合は、キーセットに依存して処理されるため、新しいオブジェクトの追加を避けてください。なお、同じプログラムでも、システムによっては異なる値が表示されることに注意してください。なぜなら、HashMapのキーセットは順序が定まっていないからです。
`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.scdev.ConcurrentModificationException;
import java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationExceptionWithArrayListSubList {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Java");
names.add("PHP");
names.add("SQL");
names.add("Angular 2");
List<String> first2Names = names.subList(0, 2);
System.out.println(names + " , " + first2Names);
names.set(1, "JavaScript");
// check the output below. :)
System.out.println(names + " , " + first2Names);
// Let's modify the list size and get 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.scdev.ConcurrentModificationException.ConcurrentModificationExceptionWithArrayListSubList.main(ConcurrentModificationExceptionWithArrayListSubList.java:26)
ArrayListのsubListのドキュメントによると、構造的な変更はsubListメソッドで返されたリストのみで許可されます。返されたリストのすべてのメソッドは、バッキングリストの実際のmodCountが期待値と等しいかどうかを最初にチェックし、等しくない場合はConcurrentModificationExceptionをスローします。
当社のGitHubリポジトリから、すべてのサンプルコードをダウンロードすることができます。