java.util.ConcurrentModificationException es una excepción muy común cuando se trabaja con clases de colección de Java. Las clases de colección de Java son rápidas en caso de fallo, lo que significa que si la colección se modifica mientras un hilo la recorre utilizando un iterador, se iterator.next()lanzará una excepción ConcurrentModificationException . La excepción de modificación concurrente puede aparecer en el caso de un entorno de programación Java de subproceso múltiple y de subproceso único.
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[]) {ListString myList = new ArrayListString();myList.add("1");myList.add("2");myList.add("3");myList.add("4");myList.add("5");IteratorString it = myList.iterator();while (it.hasNext()) {String value = it.next();System.out.println("List Value:" + value);if (value.equals("3"))myList.remove(value);}MapString, String myMap = new HashMapString, String();myMap.put("1", "1");myMap.put("2", "2");myMap.put("3", "3");IteratorString 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á un error java.util.ConcurrentModificationExceptioncuando se ejecute, como se muestra en los registros de la consola a continuación.
List Value:1List Value:2List Value:3Exception in thread "main" java.util.ConcurrentModificationExceptionat 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 del seguimiento de la pila de salida, queda claro que la excepción de modificación concurrente se lanza cuando llamamos a next()la función iteradora. Si se pregunta cómo Iterator verifica la modificación, su implementación está presente en la clase AbstractList, donde se define una variable int modCount . El modCount proporciona la cantidad de veces que se ha cambiado el tamaño de la lista. El valor modCount se usa en cada llamada next() para verificar si hay modificaciones en una función checkForComodification(). Ahora, comente la parte de la lista y ejecute el programa nuevamente. Verá que ahora no se lanza ninguna ConcurrentModificationException. Salida:
Map Value:3Map Value:2Map 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 su sistema porque el conjunto de claves de HashMap no está ordenado como una lista. Si descomentará la declaración donde estoy agregando un nuevo valor de clave en HashMap, se generará ConcurrentModificationException.
Cómo evitar ConcurrentModificationException en un entorno multiproceso
- Puede convertir la lista en una matriz y luego iterarla. Este enfoque funciona bien para listas de tamaño pequeño o mediano, pero si la lista es grande, afectará mucho el rendimiento.
- Puedes bloquear la lista mientras iteras colocándola en un bloque sincronizado. Este enfoque no se recomienda porque eliminará los beneficios del subprocesamiento múltiple.
- Si utiliza JDK1.5 o una versión posterior, puede utilizar las clases ConcurrentHashMap y CopyOnWriteArrayList . Este es el enfoque recomendado para evitar excepciones de modificación simultánea.
Cómo evitar ConcurrentModificationException en un entorno de un solo subproceso
Puede utilizar la remove()función iteradora para eliminar el objeto de la colección subyacente. Pero en este caso, puede eliminar el mismo objeto y ningún otro objeto de la lista. Ejecutemos un ejemplo utilizando 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) {ListString myList = new CopyOnWriteArrayListString();myList.add("1");myList.add("2");myList.add("3");myList.add("4");myList.add("5");IteratorString 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());MapString, String myMap = new ConcurrentHashMapString, String();myMap.put("1", "1");myMap.put("2", "2");myMap.put("3", "3");IteratorString 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 continuación se muestra el resultado del programa anterior. Puede ver que el programa no genera ninguna excepción ConcurrentModificationException.
List Value:1List Value:2List Value:3List Value:4List Value:5List Size:6Map Value:1Map Value:2Map Value:4Map Value:5Map Size:4
Del ejemplo anterior se desprende claramente que:
-
Las clases de colección concurrente se pueden modificar de forma segura, no generarán ConcurrentModificationException.
-
En el caso de CopyOnWriteArrayList, el iterador no acomoda los cambios en la lista y trabaja en la lista original.
-
En el caso de ConcurrentHashMap, el comportamiento no siempre es el mismo. Para la condición:
if(key.equals("1")){myMap.remove("3");}La salida es:
Map Value:1Map Value:nullMap Value:4Map Value:2Map Size:4Está tomando el nuevo objeto agregado con la tecla “4”, pero no el siguiente objeto agregado con la tecla “5”. Ahora, si cambio la condición a la siguiente.
if(key.equals("3")){myMap.remove("2");}La salida es:
Map Value:1Map Value:3Map Value:nullMap Size:4En este caso, no se tienen en cuenta los objetos recién añadidos. Por lo tanto, si utiliza ConcurrentHashMap, evite añadir objetos nuevos, ya que se pueden procesar según el conjunto de claves. Tenga en cuenta que el mismo programa puede imprimir diferentes valores en su sistema porque el conjunto de claves de HashMap no está ordenado.
Utilice el bucle for para evitar java.util.ConcurrentModificationException
Si está trabajando en un entorno de un solo subproceso y desea que su código se encargue de los objetos adicionales agregados en la lista, puede hacerlo utilizando un bucle for en lugar de un Iterador .
for(int i = 0; imyList.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 objeto o uno más lejano, entonces no necesita disminuir el contador. Pruébelo usted mismo. Una cosa más : obtendrá ConcurrentModificationException si intenta modificar la estructura de la lista original con subList. Veámoslo con un ejemplo simple.
package com.journaldev.ConcurrentModificationException;import java.util.ArrayList;import java.util.List;public class ConcurrentModificationExceptionWithArrayListSubList {public static void main(String[] args) {ListString names = new ArrayList();names.add("Java");names.add("PHP");names.add("SQL");names.add("Angular 2");ListString 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 ConcurrentModificationExceptionnames.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.ConcurrentModificationExceptionat 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 subList de ArrayList , las modificaciones estructurales solo se permiten en la lista devuelta por el método subList. Todos los métodos de 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 .