java.util.ConcurrentModificationException

java.util.ConcurrentModificationException – очень распространенное исключение при работе с классами коллекций Java. Классы коллекций Java являются fail-fast, что означает, что если коллекция будет изменена во время обхода некоторым потоком с использованием итератора, то iterator.next() выкинет ConcurrentModificationException. Исключение “ConcurrentModificationException” может возникнуть как в многопоточной, так и в однопоточной среде программирования на Java.

java.util.ConcurrentModificationException

Давайте рассмотрим сценарий исключения конкурентной модификации на примере.

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)

Из стека трассировки вывода ясно, что исключение при одновременной модификации возникает при вызове функции next() итератора. Если вас интересует, как итератор проверяет модификацию, реализация находится в классе 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 или выше, то вы можете использовать классы ConcurrentHashMap и CopyOnWriteArrayList. Этот подход рекомендуется для избежания исключения при одновременном изменении.

Для избежания 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 не упорядочен.

Используйте цикл for, чтобы избежать java.util.ConcurrentModificationException

Если вы работаете в однопоточной среде и хотите, чтобы ваш код заботился о дополнительных добавленных объектах в списке, то вы можете воспользоваться циклом for, а не Iterator.

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");
	}
}

Обратите внимание, что я уменьшаю счетчик, потому что удаляю тот же объект. Если вам нужно удалить следующий или еще дальше объект, то вам не нужно уменьшать счетчик. Попробуйте сами. 🙂 Еще одна важная вещь: Вы получите ConcurrentModificationException, если попытаетесь изменить структуру исходного списка с помощью subList. Давайте рассмотрим это на простом примере.

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