java.util.ConcurrentModificationException

java.util.ConcurrentModificationException é uma exceção muito comum ao trabalhar com classes de coleção em Java. As classes de coleção em Java são de detecção rápida (fail-fast), o que significa que se a coleção for alterada 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 de thread único.

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 da pilha de rastreamento 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 modCount fornece o número de vezes que o tamanho da lista foi alterado. O valor modCount é usado em cada chamada de next() para verificar se houve modificações na função checkForComodification(). Agora, comente a parte da lista e execute o programa novamente. Você verá que não está sendo lançada nenhuma ConcurrentModificationException agora. Saída:

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

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

Para evitar ConcurrentModificationException em um ambiente multithread

  1. Você pode converter a lista em um array e então iterar sobre o array. Essa abordagem funciona bem para listas pequenas ou médias, mas se a lista for grande, isso afetará muito o desempenho.
  2. Você pode travar a lista enquanto itera colocando-a em um bloco sincronizado. Essa abordagem não é recomendada porque ela anula 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 ambiente de thread único

Você pode usar a função remove() do iterator 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());
	}

}

O resultado do programa acima é mostrado 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 fica 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 de ConcurrentHashMap, o comportamento nem sempre é o mesmo. Para a condição:

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

    A saída é:

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

    Ele está considerando 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 é:

    Valor do Mapa: 1
    Valor do Mapa: 3
    Valor do Mapa: 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. Note que o mesmo programa pode imprimir valores diferentes em seu sistema porque o conjunto de chaves do HashMap não é ordenado.

Use o loop for para evitar java.util.ConcurrentModificationException

Se estiver trabalhando em um ambiente de thread único e quiser que seu código cuide dos objetos adicionados extras na lista, você pode fazer isso usando um loop for 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 diminuindo o contador porque estou removendo o mesmo objeto. Se precisar remover o próximo objeto ou um objeto ainda 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 de 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.

Você pode baixar todo o código de exemplo do nosso Repositório do GitHub.

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