¿Por qué se lanza una ConcurrentModificationException y cómo depurarla?

130

Estoy usando un Collection(un HashMapusado indirectamente por el JPA, sucede), pero aparentemente al azar el código arroja un ConcurrentModificationException. ¿Qué lo está causando y cómo soluciono este problema? ¿Utilizando alguna sincronización, tal vez?

Aquí está el seguimiento completo de la pila:

Exception in thread "pool-1-thread-1" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
        at java.util.HashMap$ValueIterator.next(Unknown Source)
        at org.hibernate.collection.AbstractPersistentCollection$IteratorProxy.next(AbstractPersistentCollection.java:555)
        at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:296)
        at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:242)
        at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:219)
        at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
        at org.hibernate.engine.Cascade.cascade(Cascade.java:130)
cuerdas principales
fuente
1
¿Puedes proporcionar más contexto? ¿Estás fusionando, actualizando o eliminando una entidad? ¿Qué asociaciones tiene esta entidad? ¿Qué pasa con su configuración en cascada?
ordnungswidrig 03 de
1
Desde el seguimiento de la pila, puede ver que la Excepción ocurre mientras se itera a través de HashMap. Seguramente algún otro hilo está modificando el mapa, pero la excepción ocurre en el hilo que está iterando.
Chochos

Respuestas:

263

Este no es un problema de sincronización. Esto ocurrirá si la colección subyacente que se está iterando es modificada por algo que no sea el iterador en sí.

Iterator it = map.entrySet().iterator();
while (it.hasNext())
{
   Entry item = it.next();
   map.remove(item.getKey());
}

Esto arrojará un ConcurrentModificationExceptioncuando it.hasNext()se llama por segunda vez.

El enfoque correcto sería

   Iterator it = map.entrySet().iterator();
   while (it.hasNext())
   {
      Entry item = it.next();
      it.remove();
   }

Asumiendo que este iterador soporta la remove()operación.

Robin
fuente
1
Posiblemente, pero parece que Hibernate está haciendo la iteración, que debería implementarse razonablemente correctamente. Podría haber una devolución de llamada modificando el mapa, pero eso es poco probable. La imprevisibilidad apunta a un problema real de concurrencia.
Tom Hawtin - tackline
Esta excepción no tiene nada que ver con la concurrencia de subprocesos, es causada por la modificación del almacén de respaldo del iterador. Si por otro hilo de no le importa al iterador. En mi humilde opinión, es una excepción mal nombrada ya que da una impresión incorrecta de la causa.
Robin
Sin embargo, estoy de acuerdo en que si es impredecible, lo más probable es que haya un problema de subprocesamiento que esté causando las condiciones para que ocurra esta excepción. Lo que lo hace aún más confuso debido al nombre de la excepción.
Robin
Esto es correcto y una mejor explicación que la respuesta aceptada, pero la respuesta aceptada es una buena solución. ConcurrentHashMap no está sujeto a CME, incluso dentro de un iterador (aunque el iterador todavía está diseñado para acceso de subproceso único).
G__
Esta solución no tiene sentido, porque Maps no tiene el método iterator (). El ejemplo de Robin sería aplicable, por ejemplo, a las listas.
Peter
72

Intenta usar un en ConcurrentHashMaplugar de un simpleHashMap

Chochos
fuente
¿Eso realmente resolvió el problema? Estoy experimentando el mismo problema, pero ciertamente puedo descartar cualquier problema de subprocesos.
tobiasbayer
55
Otra solución es crear una copia del mapa e iterar a través de esa copia. O copie el conjunto de claves e itere a través de ellas, obteniendo el valor de cada clave del mapa original.
Chochos
Es Hibernate quien está iterando a través de la colección para que no pueda simplemente copiarla.
tobiasbayer
1
Salvador instantáneo Vamos a ver por qué esto funcionó tan bien para que no tenga más sorpresas más adelante.
Valchris
1
Supongo que no es un problema de sincronización, es un problema si se modifica la misma modificación mientras se repite el mismo objeto.
Rais Alam
17

La modificación de un Collectiontiempo iterando a través de eso Collectionusando un noIterator está permitido por la mayoría de las Collectionclases. La biblioteca Java llama a un intento de modificar un Collectiontiempo mientras itera a través de ella una "modificación concurrente". Desafortunadamente, eso sugiere que la única causa posible es la modificación simultánea por múltiples hilos, pero eso no es así. Usando solo un hilo es posible crear un iterador para el Collection(usando Collection.iterator(), o un bucle mejoradofor ), comenzar a iterar (usar Iterator.next(), o ingresar de manera equivalente al cuerpo del forbucle mejorado ), modificar el Collection, luego continuar iterando.

Para ayudar a los programadores, algunas implementaciones de esas Collectionclases intentan detectar modificaciones concurrentes erróneas y arrojan un ConcurrentModificationExceptionsi lo detectan. Sin embargo, en general no es posible y práctico garantizar la detección de todas las modificaciones concurrentes. Por lo tanto, el uso erróneo de la Collectionno siempre resulta en un lanzamiento ConcurrentModificationException.

La documentación de ConcurrentModificationExceptiondice:

Esta excepción puede ser lanzada por métodos que han detectado la modificación concurrente de un objeto cuando tal modificación no está permitida ...

Tenga en cuenta que esta excepción no siempre indica que un objeto ha sido modificado simultáneamente por un hilo diferente. Si un solo hilo emite una secuencia de invocaciones de métodos que viola el contrato de un objeto, el objeto puede lanzar esta excepción ...

Tenga en cuenta que el comportamiento a prueba de fallas no puede garantizarse ya que, en términos generales, es imposible hacer garantías duras en presencia de modificaciones concurrentes no sincronizadas. Las operaciones a prueba ConcurrentModificationExceptionde fallas se basan en el mejor esfuerzo.

Tenga en cuenta que

La documentación de la HashSet, HashMap, TreeSety ArrayListclases dice lo siguiente:

Los iteradores devueltos [directa o indirectamente desde esta clase] son ​​a prueba de fallas: si la [colección] se modifica en cualquier momento después de que se crea el iterador, de cualquier manera, excepto a través del método de eliminación propio del iterador, se Iteratorlanza a ConcurrentModificationException. Por lo tanto, frente a la modificación concurrente, el iterador falla de manera rápida y limpia, en lugar de arriesgarse a un comportamiento arbitrario, no determinista en un momento indeterminado en el futuro.

Tenga en cuenta que el comportamiento a prueba de fallas de un iterador no puede garantizarse ya que, en términos generales, es imposible hacer garantías duras en presencia de modificaciones concurrentes no sincronizadas. Los iteradores a prueba de fallas se ConcurrentModificationExceptionbasan en el mejor esfuerzo. Por lo tanto, sería incorrecto escribir un programa que dependiera de esta excepción para su corrección: el comportamiento rápido de los iteradores debe usarse solo para detectar errores .

Tenga en cuenta de nuevo que el comportamiento "no se puede garantizar" y solo es "en el mejor esfuerzo".

La documentación de varios métodos de la Mapinterfaz dice esto:

Las implementaciones no concurrentes deben anular este método y, en el mejor esfuerzo, arrojar un ConcurrentModificationExceptionsi se detecta que la función de mapeo modifica este mapa durante el cálculo. Las implementaciones concurrentes deberían anular este método y, en el mejor esfuerzo, arrojar un error IllegalStateExceptionsi se detecta que la función de mapeo modifica este mapa durante el cálculo y, como resultado, el cálculo nunca se completaría.

Tenga en cuenta nuevamente que solo se requiere una "base de mejor esfuerzo" para la detección, y a ConcurrentModificationExceptionse sugiere explícitamente solo para las clases no concurrentes (no seguras para subprocesos).

Depuración ConcurrentModificationException

Por lo tanto, cuando ve un seguimiento de pila debido a a ConcurrentModificationException, no puede suponer de inmediato que la causa es el acceso no seguro de subprocesos múltiples a a Collection. Debe examinar el seguimiento de la pila para determinar qué clase de Collectionlanzó la excepción (un método de la clase la habrá arrojado directa o indirectamente) y para qué Collectionobjeto. Luego debe examinar desde dónde se puede modificar ese objeto.

  • La causa más común es la modificación de Collectiondentro de un forbucle mejorado sobre Collection. ¡El hecho de que no vea un Iteratorobjeto en su código fuente no significa que no haya Iteratorallí! Afortunadamente, una de las declaraciones del forbucle defectuoso generalmente estará en el seguimiento de la pila, por lo que rastrear el error suele ser fácil.
  • Un caso más complicado es cuando su código pasa referencias al Collectionobjeto. Tenga en cuenta que las vistas no modificables de las colecciones (como las producidas por Collections.unmodifiableList()) retienen una referencia a la colección modificable, por lo que la iteración sobre una colección "no modificable" puede arrojar la excepción (la modificación se ha realizado en otro lugar). Otras vistas de su Collection, como sublistas , Mapconjuntos de entradas y Mapconjuntos de claves también conservan referencias al original (modificable) Collection. Esto puede ser un problema incluso para un hilo seguro Collection, como CopyOnWriteList; no asuma que las colecciones seguras para hilos (concurrentes) nunca pueden lanzar la excepción.
  • Las operaciones que pueden modificar a Collectionpueden ser inesperadas en algunos casos. Por ejemplo, LinkedHashMap.get()modifica su colección .
  • Los casos más difíciles son cuando la excepción se debe a la modificación concurrente por múltiples hilos.

Programación para evitar errores de modificación concurrentes

Cuando sea posible, limite todas las referencias a un Collectionobjeto, para que sea más fácil evitar modificaciones concurrentes. Convierta Collectionun privateobjeto o una variable local y no devuelva referencias a Collectionsus iteradores de los métodos. Entonces es mucho más fácil examinar todos los lugares donde Collectionse pueden modificar. Si la Collectionvan a utilizar varios subprocesos, entonces es práctico asegurarse de que los subprocesos accedan Collectionsolo con la sincronización y el bloqueo adecuados.

Raedwald
fuente
Me pregunto por qué no se permite la modificación concurrente en caso de un solo hilo. ¿Qué problemas pueden ocurrir si se permite que un solo hilo realice una modificación concurrente en un mapa hash regular?
MasterJoe
4

En Java 8, puede usar la expresión lambda:

map.keySet().removeIf(key -> key condition);
Zentopia
fuente
2

Suena menos como un problema de sincronización de Java y más como un problema de bloqueo de la base de datos.

No sé si agregar una versión a todas sus clases persistentes lo resolverá, pero esa es una forma en que Hibernate puede proporcionar acceso exclusivo a las filas de una tabla.

Podría ser que el nivel de aislamiento necesita ser más alto. Si permite "lecturas sucias", tal vez necesite subir a serializable.

duffymo
fuente
Creo que se referían a Hashtable. Se envió como parte de JDK 1.0. Al igual que Vector, fue escrito para ser seguro para subprocesos, y lento. Ambos han sido reemplazados por alternativas no seguras para subprocesos: HashMap y ArrayList. Paga por lo que usas.
duffymo
0

Pruebe CopyOnWriteArrayList o CopyOnWriteArraySet dependiendo de lo que esté tratando de hacer.

Javamann
fuente
0

Tenga en cuenta que la respuesta seleccionada no se puede aplicar a su contexto directamente antes de alguna modificación, si está tratando de eliminar algunas entradas del mapa mientras itera el mapa como yo.

Solo doy mi ejemplo de trabajo aquí para que los novatos ahorren su tiempo:

HashMap<Character,Integer> map=new HashMap();
//adding some entries to the map
...
int threshold;
//initialize the threshold
...
Iterator it=map.entrySet().iterator();
while(it.hasNext()){
    Map.Entry<Character,Integer> item=(Map.Entry<Character,Integer>)it.next();
    //it.remove() will delete the item from the map
    if((Integer)item.getValue()<threshold){
        it.remove();
    }
ZhaoGang
fuente
0

Me encontré con esta excepción cuando intento eliminar x últimos elementos de la lista. myList.subList(lastIndex, myList.size()).clear();fue la única solución que funcionó para mí.

hombre gris
fuente