java.util.ConcurrentModificationException

java.util.ConcurrentModificationException es una excepción muy común al trabajar con clases de colecciones en Java. Las clases de colección de Java son de tipo fail-fast, lo que significa que si la colección cambia mientras algún hilo la está recorriendo usando un iterador, el iterator.next() lanzará una ConcurrentModificationException. La excepción de modificación concurrente puede ocurrir tanto en entornos de programación Java multihilo como en entornos de programación de un solo hilo.

java.util.ConcurrentModificationException

Veamos el escenario de excepción de modificación concurrente con un ejemplo.

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

	}
}

El programa anterior lanzará java.util.ConcurrentModificationException cuando se ejecute, como se muestra en los registros de la consola a continuación.

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)

Desde la pila de rastreo de salida, está claro que la excepción de modificación concurrente se lanza cuando llamamos a la función next() del iterador. Si te estás preguntando cómo el Iterador verifica la modificación, su implementación está presente en la clase AbstractList, donde se define una variable int modCount. El modCount proporciona el número de veces que el tamaño de la lista ha cambiado. El valor de modCount se utiliza en cada llamada a next() para verificar si ha habido modificaciones en una función checkForComodification(). Ahora, comenta la parte de la lista y ejecuta el programa nuevamente. Verás que ahora no se lanza ninguna ConcurrentModificationException. Salida:

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

Dado que estamos actualizando el valor de clave existente en myMap, su tamaño no ha cambiado y no estamos obteniendo ConcurrentModificationException. La salida puede ser diferente en tu sistema porque el conjunto de claves de HashMap no está ordenado como una Lista. Si descomentas la declaración donde estoy agregando un nuevo par clave-valor en el HashMap, causará ConcurrentModificationException.

Para evitar ConcurrentModificationException en un entorno multi-hilo

  1. Puedes convertir la lista en un arreglo y luego iterar sobre el arreglo. Este enfoque funciona bien para listas pequeñas o medianas, pero si la lista es grande, afectará mucho el rendimiento.
  2. Puede bloquear la lista mientras itera colocándola en un bloque sincronizado. Este enfoque no se recomienda porque anulará los beneficios de la programación multiproceso.
  3. Si está utilizando JDK1.5 o superior, puede usar las clases ConcurrentHashMap y CopyOnWriteArrayList. Este es el enfoque recomendado para evitar la excepción de modificación concurrente.

Para evitar ConcurrentModificationException en un entorno de un solo subproceso

Puede utilizar la función remove() del iterador para eliminar el objeto del objeto de colección subyacente. Pero en este caso, puede eliminar el mismo objeto y no cualquier otro objeto de la lista. Veamos un ejemplo usando clases de colección concurrentes.

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

}

La salida del programa anterior se muestra a continuación. Puede ver que no se produce ConcurrentModificationException en el 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 del ejemplo anterior, está claro que:

  1. Las clases de colección concurrentes se pueden modificar de forma segura, no lanzarán ConcurrentModificationException.

  2. En el caso de CopyOnWriteArrayList, el iterador no tiene en cuenta los cambios en la lista y funciona en la lista original.

  3. En caso de ConcurrentHashMap, el comportamiento no siempre es el mismo. Para la condición:

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

    La salida es:

    Valor del Mapa: 1
    Valor del Mapa: nulo
    Valor del Mapa: 4
    Valor del Mapa: 2
    Tamaño del Mapa: 4
    

    Está tomando el nuevo objeto agregado con la clave “4” pero no el siguiente objeto agregado con la clave “5”. Ahora, si cambio la condición a lo siguiente.

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

    La salida es:

    Valor del Mapa: 1
    Valor del Mapa: 3
    Valor del Mapa: nulo
    Tamaño del Mapa: 4
    

    En este caso, no está considerando los objetos recién agregados. Entonces, si estás usando ConcurrentHashMap, evita agregar nuevos objetos ya que pueden ser procesados dependiendo del conjunto de claves. Ten en cuenta que el mismo programa puede imprimir valores diferentes en tu sistema porque el conjunto de claves de HashMap no está ordenado.

Utilice un bucle for para evitar java.util.ConcurrentModificationException

Si está trabajando en un entorno de un solo hilo y desea que su código se encargue de los objetos añadidos extra en la lista, puede hacerlo usando un bucle for en lugar de un 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");
	}
}

Tenga en cuenta que estoy disminuyendo el contador porque estoy eliminando el mismo objeto; si tiene que eliminar el siguiente u otro objeto más lejano, no es necesario disminuir el contador. ¡Inténtalo tú mismo! 🙂 Otra cosa: Obtendrá ConcurrentModificationException si intenta modificar la estructura de la lista original con subList. Veámoslo con un ejemplo sencillo.

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");
		// comprueba la salida a continuación. :)
		System.out.println(names + " , " + first2Names);

		// Modifiquemos el tamaño de la lista y obtengamos ConcurrentModificationException
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // this line throws exception

	}

}

La salida del programa anterior es:

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

Según la documentación de ArrayList subList, las modificaciones estructurales solo están permitidas en la lista devuelta por el método subList. Todos los métodos en la lista devuelta primero verifican si el modCount real de la lista de respaldo es igual a su valor esperado y lanzan una ConcurrentModificationException si no lo es.

Puedes descargar todo el código de ejemplo desde nuestro Repositorio de GitHub.

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