Pourquoi une ConcurrentModificationException est-elle levée et comment la déboguer

130

J'utilise a Collection(un HashMaputilisé indirectement par le JPA, c'est le cas), mais apparemment de manière aléatoire, le code lance un ConcurrentModificationException. Quelle en est la cause et comment résoudre ce problème? En utilisant une certaine synchronisation, peut-être?

Voici le stack-trace complet:

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)
mainstringargs
la source
1
Pouvez-vous fournir un peu plus de contexte? Vous fusionnez, mettez à jour ou supprimez une entité? Quelles associations cette entité a-t-elle? Qu'en est-il de vos paramètres en cascade?
ordnungswidrig
1
À partir de la trace de la pile, vous pouvez voir que l'exception se produit lors de l'itération dans le HashMap. Un autre thread est sûrement en train de modifier la carte, mais l'exception se produit dans le thread en cours d'itération.
Chochos

Réponses:

263

Ce n'est pas un problème de synchronisation. Cela se produira si la collection sous-jacente en cours d'itération est modifiée par autre chose que l'itérateur lui-même.

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

Cela lancera un ConcurrentModificationExceptionlorsque le it.hasNext()sera appelé la deuxième fois.

La bonne approche serait

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

En supposant que cet itérateur prend en charge l' remove()opération.

Robin
la source
1
Peut-être, mais il semble qu'Hibernate effectue l'itération, qui devrait être implémentée raisonnablement correctement. Il pourrait y avoir un rappel modifiant la carte, mais c'est peu probable. L'imprévisibilité indique un problème de concurrence réelle.
Tom Hawtin - tackline
Cette exception n'a rien à voir avec la concurrence des threads, elle est provoquée par la modification du magasin de stockage de l'itérateur. Que ce soit par un autre thread ou non n'a pas d'importance pour l'itérateur. IMHO c'est une exception mal nommée car elle donne une impression incorrecte de la cause.
Robin
Je conviens cependant que s'il est imprévisible, il y a très probablement un problème de threading qui provoque les conditions de cette exception. Ce qui rend les choses encore plus déroutantes à cause du nom de l'exception.
Robin
C'est correct et une meilleure explication que la réponse acceptée, mais la réponse acceptée est une bonne solution. ConcurrentHashMap n'est pas soumis à CME, même à l'intérieur d'un itérateur (bien que l'itérateur soit toujours conçu pour un accès à un seul thread).
G__
Cette solution n'a aucun intérêt, car Maps n'a pas de méthode iterator (). L'exemple de Robin serait applicable, par exemple, aux listes.
peter
72

Essayez d'utiliser un ConcurrentHashMapplutôt qu'un simpleHashMap

Chochos
la source
Cela a-t-il vraiment résolu le problème? Je rencontre le même problème, mais je peux très certainement exclure tout problème de filetage.
tobiasbayer
5
Une autre solution consiste à créer une copie de la carte et à parcourir cette copie à la place. Ou copiez l'ensemble de clés et parcourez-les, en obtenant la valeur de chaque clé à partir de la carte d'origine.
Chochos
C'est Hibernate qui itère dans la collection, vous ne pouvez donc pas simplement la copier.
tobiasbayer
1
Sauveur instantané. Je vais voir pourquoi cela a si bien fonctionné pour ne pas avoir plus de surprises plus tard.
Valchris
1
Je suppose que ce n'est pas un problème de synchronisation, c'est un problème si la modification de la même modification en boucle le même objet.
Rais Alam
17

La modification d'une Collectionitération while à l' Collectionaide de an Iteratorn'est pas autorisée par la plupart des Collectionclasses. La bibliothèque Java appelle une tentative de modification d'un Collectiontout en l'itérant une "modification simultanée". Cela suggère malheureusement que la seule cause possible est la modification simultanée par plusieurs threads, mais ce n'est pas le cas. En utilisant un seul thread, il est possible de créer un itérateur pour Collection(en utilisant Collection.iterator(), ou une boucle amélioréefor ), de démarrer l'itération (en utilisant Iterator.next()ou en entrant de manière équivalente le corps de la forboucle améliorée ), de modifier le Collection, puis de continuer l'itération.

Pour aider les programmeurs, certaines implémentations de ces Collectionclasses tentent de détecter une modification concurrente erronée et lancent un ConcurrentModificationExceptionsi elles la détectent. Cependant, il n'est en général pas possible et pratique de garantir la détection de toutes les modifications concurrentes. Ainsi, une utilisation erronée de l ' Collectionn'entraîne pas toujours un jet ConcurrentModificationException.

La documentation de ConcurrentModificationExceptiondit:

Cette exception peut être levée par des méthodes qui ont détecté une modification concurrente d'un objet lorsqu'une telle modification n'est pas autorisée ...

Notez que cette exception n'indique pas toujours qu'un objet a été modifié simultanément par un thread différent. Si un seul thread émet une séquence d'appels de méthode qui viole le contrat d'un objet, l'objet peut lever cette exception ...

Notez que le comportement à échec rapide ne peut pas être garanti car il est, d'une manière générale, impossible de faire des garanties fermes en présence de modifications concurrentes non synchronisées. Les opérations échouées sont basées ConcurrentModificationExceptionsur le meilleur effort.

Notez que

La documentation du HashSet, HashMap, TreeSetet les ArrayListclasses dit ceci:

Les itérateurs retournés [directement ou indirectement de cette classe] sont rapides: si la [collection] est modifiée à tout moment après la création de l'itérateur, de quelque manière que ce soit sauf via la propre méthode remove de l'itérateur, la commande Iteratorrenvoie un ConcurrentModificationException. Ainsi, face à une modification concurrente, l'itérateur échoue rapidement et proprement, plutôt que de risquer un comportement arbitraire et non déterministe à un moment indéterminé dans le futur.

Notez que le comportement de défaillance rapide d'un itérateur ne peut pas être garanti car il est, d'une manière générale, impossible de faire des garanties matérielles en présence de modifications concurrentes non synchronisées. Les itérateurs rapides échouent ConcurrentModificationExceptionsur la base du meilleur effort. Par conséquent, il serait erroné d'écrire un programme qui dépendait de cette exception pour son exactitude: le comportement rapide des itérateurs ne devrait être utilisé que pour détecter les bogues .

Notez à nouveau que le comportement «ne peut être garanti» et n'est que «au mieux».

La documentation de plusieurs méthodes de l' Mapinterface dit ceci:

Les implémentations non simultanées devraient remplacer cette méthode et, au mieux, lancer un ConcurrentModificationExceptions'il est détecté que la fonction de mappage modifie cette carte pendant le calcul. Les implémentations concurrentes doivent remplacer cette méthode et, au mieux, lancer un IllegalStateExceptions'il est détecté que la fonction de mappage modifie cette carte pendant le calcul et que, par conséquent, le calcul ne se terminerait jamais.

Notez à nouveau que seule une "base de meilleur effort" est requise pour la détection, et a ConcurrentModificationExceptionn'est explicitement suggéré que pour les classes non concurrentes (non thread-safe).

Débogage ConcurrentModificationException

Ainsi, lorsque vous voyez une trace de pile due à a ConcurrentModificationException, vous ne pouvez pas immédiatement supposer que la cause est un accès multithread non sécurisé à un fichier Collection. Vous devez examiner la trace de pile pour déterminer quelle classe de a Collectionlancé l'exception (une méthode de la classe l'aura directement ou indirectement lancée), et pour quel Collectionobjet. Ensuite, vous devez examiner d'où cet objet peut être modifié.

  • La cause la plus fréquente est la modification de l' Collectionintérieur d'une forboucle améliorée sur le Collection. Ce n'est pas parce que vous ne voyez pas d' Iteratorobjet dans votre code source qu'il n'y en a pas Iterator! Heureusement, l'une des instructions de la forboucle défectueuse se trouve généralement dans la trace de la pile, il est donc généralement facile de localiser l'erreur.
  • Un cas plus délicat est celui où votre code passe autour des références à l' Collectionobjet. Notez que les vues non modifiables des collections (telles que produites par Collections.unmodifiableList()) conservent une référence à la collection modifiable, donc l' itération sur une collection "non modifiable" peut lever l'exception (la modification a été faite ailleurs). D'autres vues de votre Collection, telles que les sous-listes , Maples jeux d'entrées et Maples jeux de clés conservent également des références à l'original (modifiable) Collection. Cela peut être un problème même pour un thread-safe Collection, tel que CopyOnWriteList; ne supposez pas que les collections thread-safe (simultanées) ne peuvent jamais lever l'exception.
  • Les opérations qui peuvent modifier un Collectionpeuvent être inattendues dans certains cas. Par exemple, LinkedHashMap.get()modifie sa collection .
  • Les cas les plus difficiles sont ceux où l'exception est due à une modification simultanée par plusieurs threads.

Programmation pour éviter les erreurs de modification simultanées

Lorsque cela est possible, limitez toutes les références à un Collectionobjet, il est donc plus facile d'empêcher les modifications simultanées. Créez Collectionun privateobjet ou une variable locale et ne renvoyez pas de références à Collectionou à ses itérateurs à partir des méthodes. Il est alors beaucoup plus facile d'examiner tous les endroits où le Collectionpeut être modifié. Si le Collectiondoit être utilisé par plusieurs threads, il est alors pratique de s'assurer que les threads accèdent au Collectionseul avec une synchronisation et un verrouillage appropriés.

Raedwald
la source
Je me demande pourquoi la modification simultanée n'est pas autorisée dans le cas d'un seul thread. Quels problèmes peuvent survenir si un seul thread est autorisé à effectuer une modification simultanée sur une carte de hachage régulière?
MasterJoe
4

Dans Java 8, vous pouvez utiliser l'expression lambda:

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

Cela ressemble moins à un problème de synchronisation Java qu'à un problème de verrouillage de base de données.

Je ne sais pas si l'ajout d'une version à toutes vos classes persistantes résoudra le problème, mais c'est une façon pour Hibernate de fournir un accès exclusif aux lignes d'une table.

Peut-être que le niveau d'isolement doit être plus élevé. Si vous autorisez les "lectures sales", vous devrez peut-être passer à sérialisable.

duffymo
la source
Je pense qu'ils voulaient dire Hashtable. Il a été livré dans le cadre de JDK 1.0. Comme Vector, il a été écrit pour être thread-safe - et lent. Les deux ont été remplacés par des alternatives non thread-safe: HashMap et ArrayList. Payez ce que vous utilisez.
duffymo
0

Essayez CopyOnWriteArrayList ou CopyOnWriteArraySet en fonction de ce que vous essayez de faire.

Javamann
la source
0

Notez que la réponse sélectionnée ne peut pas être appliquée à votre contexte directement avant une modification, si vous essayez de supprimer certaines entrées de la carte tout en itérant la carte comme moi.

Je donne juste mon exemple de travail ici pour les débutants pour gagner du temps:

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
la source
0

J'ai rencontré cette exception en essayant de supprimer x derniers éléments de la liste. myList.subList(lastIndex, myList.size()).clear();était la seule solution qui fonctionnait pour moi.

homme gris
la source