java.util.ConcurrentModificationException

java.util.ConcurrentModificationException é uma exceção muito comum ao trabalhar com classes de coleção Java. As classes de coleção Java são fail-fast, o que significa que se a coleção for modificada enquanto algum thread estiver percorrendo-a usando um iterador, o iterator.next() lançará ConcurrentModificationException. A exceção de modificação concorrente pode ocorrer tanto em ambientes de programação Java multithread quanto em ambientes de programação Java single-threaded.

java.util.ConcurrentModificationException

Vamos ver o cenário de exceção de modificação concorrente com um exemplo.

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

	}
}

O programa acima lançará java.util.ConcurrentModificationException quando executado, como mostrado nos logs do console abaixo.

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)

A partir do rastreamento de pilha de saída, é claro que a exceção de modificação concorrente é lançada quando chamamos a função next() do iterador. Se você está se perguntando como o Iterator verifica a modificação, sua implementação está presente na classe AbstractList, onde uma variável int modCount é definida. O valor de modCount é usado em cada chamada de next() para verificar quaisquer modificações na função checkForComodification(). Agora, comente a parte da lista e execute o programa novamente. Você verá que nenhuma ConcurrentModificationException é lançada agora. Saída:

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

Como estamos atualizando o valor da chave existente no myMap, seu tamanho não foi alterado e não estamos obtendo ConcurrentModificationException. A saída pode ser diferente em seu sistema porque HashMap keyset não é ordenado como uma lista. Se descomentar a instrução onde estou adicionando um novo par chave-valor no HashMap, isso causará uma ConcurrentModificationException.

Para evitar ConcurrentModificationException em um ambiente multithread

  1. Você pode converter a lista para um array e depois iterar sobre o array. Essa abordagem funciona bem para listas pequenas ou médias, mas se a lista for grande, afetará muito o desempenho.
  2. Você pode bloquear a lista enquanto itera colocando-a em um bloco sincronizado. Esta abordagem não é recomendada porque cessará os benefícios da multithreading.
  3. Se você estiver usando JDK1.5 ou superior, então você pode usar as classes ConcurrentHashMap e CopyOnWriteArrayList. Esta é a abordagem recomendada para evitar a exceção de modificação concorrente.

Para evitar ConcurrentModificationException em um ambiente com um único thread

Você pode usar a função remove() do iterador para remover o objeto do objeto de coleção subjacente. Mas, neste caso, você pode remover o mesmo objeto e não qualquer outro objeto da lista. Vamos executar um exemplo usando classes de Coleção Concorrente.

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

}

A saída do programa acima é mostrada abaixo. Você pode ver que não há ConcurrentModificationException sendo lançada pelo programa.

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

A partir do exemplo acima, está claro que:

  1. As classes de Coleção Concorrente podem ser modificadas com segurança, elas não lançarão ConcurrentModificationException.

  2. No caso de CopyOnWriteArrayList, o iterador não acomoda as alterações na lista e funciona na lista original.

  3. No caso do ConcurrentHashMap, o comportamento nem sempre é o mesmo. Para a condição:

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

    A saída é:

    Mapa Valor:1
    Mapa Valor:null
    Mapa Valor:4
    Mapa Valor:2
    Tamanho do Mapa:4
    

    Ele está pegando o novo objeto adicionado com a chave “4”, mas não o próximo objeto adicionado com a chave “5”. Agora, se eu mudar a condição para o seguinte:

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

    A saída é:

    Mapa Valor:1
    Mapa Valor:3
    Mapa Valor:null
    Tamanho do Mapa:4
    

    Neste caso, ele não está considerando os objetos recém-adicionados. Portanto, se você estiver usando ConcurrentHashMap, evite adicionar novos objetos, pois eles podem ser processados dependendo do conjunto de chaves. Observe que o mesmo programa pode imprimir valores diferentes em seu sistema, pois o conjunto de chaves do HashMap não é ordenado.

Use for loop to avoid java.util.ConcurrentModificationException

Se estiver a trabalhar num ambiente de uma única thread e deseja que o seu código cuide dos objetos adicionados extra na lista, pode fazê-lo utilizando um loop em vez de um 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");
	}
}

Observe que estou a diminuir o contador porque estou a remover o mesmo objeto. Se precisar de remover o próximo ou um objeto mais distante, não é necessário diminuir o contador. Experimente por si mesmo. 🙂 Mais uma coisa: Você receberá ConcurrentModificationException se tentar modificar a estrutura da lista original com subList. Vamos ver isso com um exemplo simples.

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");
		// Verifique a saída abaixo. :)
		System.out.println(names + " , " + first2Names);

		// Vamos modificar o tamanho da lista e obter ConcurrentModificationException
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // this line throws exception

	}

}

A saída do programa acima é:

[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)

De acordo com a documentação de subList da ArrayList, as modificações estruturais são permitidas apenas na lista retornada pelo método subList. Todos os métodos na lista retornada primeiro verificam se o modCount real da lista de suporte é igual ao seu valor esperado e lançam uma ConcurrentModificationException se não for.

Pode baixar todo o código de exemplo do nosso Repositório do GitHub.

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