Warum wird eine ConcurrentModificationException ausgelöst und wie wird sie debuggt?

130

Ich benutze a Collection(a HashMapwird indirekt von der JPA verwendet, es passiert also), aber anscheinend wirft der Code zufällig a ConcurrentModificationException. Was verursacht es und wie behebe ich dieses Problem? Vielleicht durch Synchronisation?

Hier ist die vollständige Stapelverfolgung:

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)
Hauptstränge
quelle
1
Können Sie mehr Kontext bereitstellen? Führen Sie eine Entität zusammen, aktualisieren oder löschen Sie sie? Welche Assoziationen hat diese Entität? Was ist mit Ihren Kaskadeneinstellungen?
Besitzwidrig
1
An der Stapelverfolgung können Sie erkennen, dass die Ausnahme beim Durchlaufen der HashMap auftritt. Sicherlich ändert ein anderer Thread die Map, aber die Ausnahme tritt in dem Thread auf, der iteriert.
Chochos

Antworten:

263

Dies ist kein Synchronisationsproblem. Dies tritt auf, wenn die zugrunde liegende Sammlung, über die iteriert wird, durch etwas anderes als den Iterator selbst geändert wird.

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

Dies wird ein werfen, ConcurrentModificationExceptionwenn das it.hasNext()das zweite Mal aufgerufen wird.

Der richtige Ansatz wäre

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

Angenommen, dieser Iterator unterstützt die remove()Operation.

Robin
quelle
1
Möglicherweise, aber es sieht so aus, als würde Hibernate die Iteration durchführen, die einigermaßen korrekt implementiert werden sollte. Es könnte einen Rückruf geben, der die Karte ändert, aber das ist unwahrscheinlich. Die Unvorhersehbarkeit weist auf ein tatsächliches Parallelitätsproblem hin.
Tom Hawtin - Tackline
Diese Ausnahme hat nichts mit Threading-Parallelität zu tun. Sie wird dadurch verursacht, dass der Sicherungsspeicher des Iterators geändert wird. Ob von einem anderen Thread oder nicht, spielt für den Iterator keine Rolle. IMHO ist es eine schlecht benannte Ausnahme, da es einen falschen Eindruck von der Ursache gibt.
Robin
Ich bin jedoch damit einverstanden, dass, wenn dies nicht vorhersehbar ist, höchstwahrscheinlich ein Threading-Problem vorliegt, das dazu führt, dass die Bedingungen für diese Ausnahme auftreten. Das macht es wegen des Ausnahmenamens umso verwirrender.
Robin
Dies ist richtig und eine bessere Erklärung als die akzeptierte Antwort, aber die akzeptierte Antwort ist eine nette Lösung. ConcurrentHashMap unterliegt auch innerhalb eines Iterators keiner CME (obwohl der Iterator immer noch für den Single-Thread-Zugriff ausgelegt ist).
G__
Diese Lösung hat keinen Sinn, da Maps keine iterator () -Methode haben. Robins Beispiel wäre zB auf Listen anwendbar.
Peter
72

Versuchen Sie es mit einem ConcurrentHashMapanstelle einer EbeneHashMap

Chochos
quelle
Hat das das Problem wirklich gelöst? Ich habe das gleiche Problem, kann aber Threading-Probleme mit Sicherheit ausschließen.
Tobiasbayer
5
Eine andere Lösung besteht darin, eine Kopie der Karte zu erstellen und diese Kopie stattdessen zu durchlaufen. Oder kopieren Sie den Schlüsselsatz und durchlaufen Sie ihn, wobei Sie den Wert für jeden Schlüssel aus der Originalkarte abrufen.
Chochos
Es ist Hibernate, der die Sammlung durchläuft, sodass Sie sie nicht einfach kopieren können.
Tobiasbayer
1
Sofortiger Retter. Ich werde untersuchen, warum dies so gut funktioniert hat, damit ich später keine weiteren Überraschungen mehr bekomme.
Valchris
1
Ich denke, es ist kein Synchronisationsproblem. Es ist ein Problem, wenn dieselbe Änderung geändert wird, während dasselbe Objekt wiederholt wird.
Rais Alam
17

Eine Änderung einer CollectionWeile, die durch die CollectionVerwendung von a iteriert, Iteratorist von den meisten Klassen nicht zulässigCollection . Die Java-Bibliothek nennt einen Änderungsversuch Collectionwährend des Durchlaufens eine "gleichzeitige Änderung". Das deutet leider darauf hin, dass die einzig mögliche Ursache die gleichzeitige Änderung durch mehrere Threads ist, aber das ist nicht so. Mit nur einem Thread ist es möglich, einen Iterator für die Collection(using Collection.iterator()oder eine erweiterte forSchleife ) zu erstellen , mit der Iteration zu beginnen (mit Iterator.next()oder gleichwertig in den Körper der erweiterten forSchleife einzutreten ), die zu ändern Collectionund dann die Iteration fortzusetzen.

Um Programmierern zu helfen, versuchen einige Implementierungen dieser CollectionKlassen , fehlerhafte gleichzeitige Änderungen zu erkennen, und werfen ein, wenn sie dies erkennen. Es ist jedoch im Allgemeinen nicht möglich und praktisch, die Erkennung aller gleichzeitigen Änderungen zu gewährleisten. Eine fehlerhafte Verwendung des führt also nicht immer zu einem WurfConcurrentModificationExceptionCollectionConcurrentModificationException .

Die Dokumentation von ConcurrentModificationExceptionsagt:

Diese Ausnahme kann durch Methoden ausgelöst werden, die eine gleichzeitige Änderung eines Objekts festgestellt haben, wenn eine solche Änderung nicht zulässig ist ...

Beachten Sie, dass diese Ausnahme nicht immer anzeigt, dass ein Objekt gleichzeitig von einem anderen Thread geändert wurde. Wenn ein einzelner Thread eine Folge von Methodenaufrufen ausgibt, die gegen den Vertrag eines Objekts verstoßen, kann das Objekt diese Ausnahme auslösen ...

Beachten Sie, dass ein ausfallsicheres Verhalten nicht garantiert werden kann, da es im Allgemeinen unmöglich ist, bei nicht synchronisierten gleichzeitigen Änderungen harte Garantien zu geben. Fail-Fast-Operationen werden ConcurrentModificationExceptionnach besten Kräften ausgeführt.

Beachten Sie, dass

Die Dokumentation der HashSet, HashMap, TreeSetund ArrayListKlassen , sagt dazu:

Die [direkt oder indirekt von dieser Klasse] zurückgegebenen Iteratoren sind ausfallsicher: Wenn die [Sammlung] zu irgendeinem Zeitpunkt nach dem Erstellen des Iterators geändert wird, außer durch die eigene Entfernungsmethode des Iterators, werden a IteratorausgelöstConcurrentModificationException . Daher fällt der Iterator angesichts gleichzeitiger Änderungen schnell und sauber aus, anstatt zu einem unbestimmten Zeitpunkt in der Zukunft willkürliches, nicht deterministisches Verhalten zu riskieren.

Beachten Sie, dass das ausfallsichere Verhalten eines Iterators nicht garantiert werden kann, da es im Allgemeinen unmöglich ist, bei nicht synchronisierten gleichzeitigen Änderungen harte Garantien zu geben. Fail-Fast-Iteratoren werfen ConcurrentModificationExceptionnach besten Kräften. Daher wäre es falsch, ein Programm zu schreiben, dessen Richtigkeit von dieser Ausnahme abhängt: Das ausfallsichere Verhalten von Iteratoren sollte nur zur Erkennung von Fehlern verwendet werden .

Beachten Sie erneut, dass das Verhalten "nicht garantiert werden kann" und nur "auf Best-Effort-Basis" ist.

Die Dokumentation mehrerer Methoden der MapSchnittstelle besagt Folgendes:

Nicht gleichzeitige Implementierungen sollten diese Methode überschreiben und nach bestem Wissen und Gewissen ein auslösen, ConcurrentModificationExceptionwenn festgestellt wird, dass die Zuordnungsfunktion diese Zuordnung während der Berechnung ändert. Gleichzeitige Implementierungen sollten diese Methode überschreiben und nach bestem Wissen und Gewissen eine auslösen, IllegalStateExceptionwenn festgestellt wird, dass die Zuordnungsfunktion diese Zuordnung während der Berechnung ändert und die Berechnung daher niemals abgeschlossen werden würde.

Beachten Sie erneut, dass für die Erkennung nur eine "Best-Effort-Basis" erforderlich ist und a ConcurrentModificationExceptionexplizit nur für nicht gleichzeitige (nicht threadsichere) Klassen empfohlen wird.

Debuggen ConcurrentModificationException

Wenn Sie also aufgrund von a einen Stack-Trace sehen ConcurrentModificationException, können Sie nicht sofort davon ausgehen, dass die Ursache ein unsicherer Multithread-Zugriff auf a ist Collection. Sie müssen den Stack-Trace untersuchen, um festzustellen, welche Klasse Collectiondie Ausnahme ausgelöst hat (eine Methode der Klasse hat sie direkt oder indirekt ausgelöst) und für welches CollectionObjekt. Dann müssen Sie prüfen, von wo aus das Objekt geändert werden kann.

  • Die häufigste Ursache ist die Änderung der Collectioninnerhalb einer erweiterten forSchleife über die Collection. Nur weil Sie Iteratorin Ihrem Quellcode kein Objekt sehen, heißt das nicht, dass es dort kein Iteratorgibt! Glücklicherweise befindet sich eine der Anweisungen der fehlerhaften forSchleife normalerweise in der Stapelverfolgung, sodass das Aufspüren des Fehlers normalerweise einfach ist.
  • Ein schwierigerer Fall ist, wenn Ihr Code Verweise auf das CollectionObjekt weitergibt . Beachten Sie, dass nicht modifizierbare Ansichten von Sammlungen (wie z. B. von Collections.unmodifiableList()) einen Verweis auf die modifizierbare Sammlung beibehalten, sodass eine Iteration über eine "nicht modifizierbare" Sammlung die Ausnahme auslösen kann (die Änderung wurde an anderer Stelle vorgenommen). Andere Ansichten von Ihnen Collection, wie Unterlisten , MapEintragssätze und MapSchlüsselsätze, behalten ebenfalls Verweise auf das Original bei (änderbar) Collection. Dies kann selbst für einen Thread-Safe ein Problem sein Collection, wie z CopyOnWriteList. Gehen Sie nicht davon aus, dass threadsichere (gleichzeitige) Sammlungen niemals die Ausnahme auslösen können.
  • Welche Operationen a ändern Collectionkönnen, kann in einigen Fällen unerwartet sein. Ändert beispielsweise LinkedHashMap.get()die Sammlung .
  • Die schwersten Fälle sind , wenn die Ausnahme ist , um gleichzeitige Modifikation von mehreren Threads durch.

Programmierung zur Vermeidung gleichzeitiger Änderungsfehler

Beschränken Sie nach Möglichkeit alle Verweise auf ein CollectionObjekt, damit gleichzeitige Änderungen leichter verhindert werden können. Erstellen Sie Collectionein privateObjekt oder eine lokale Variable und geben Sie keine Verweise auf die Collectionoder ihre Iteratoren von Methoden zurück. Es ist dann viel einfacher, alle Stellen zu untersuchen, an denen CollectionÄnderungen vorgenommen werden können. Wenn das Collectionvon mehreren Threads verwendet werden soll, ist es praktisch sicherzustellen, dass die Threads Collectionnur mit entsprechender Synchronisation und Sperrung auf die Threads zugreifen .

Raedwald
quelle
Ich frage mich, warum die gleichzeitige Änderung bei einem einzelnen Thread nicht zulässig ist. Welche Probleme können auftreten, wenn ein einzelner Thread eine gleichzeitige Änderung an einer regulären Hash-Map vornehmen darf?
MasterJoe2
4

In Java 8 können Sie den Lambda-Ausdruck verwenden:

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

Es klingt weniger nach einem Java-Synchronisationsproblem als nach einem Problem mit der Datenbanksperre.

Ich weiß nicht, ob das Hinzufügen einer Version zu all Ihren persistenten Klassen das Problem lösen wird, aber auf diese Weise kann Hibernate exklusiven Zugriff auf Zeilen in einer Tabelle gewähren.

Könnte sein, dass die Isolationsstufe höher sein muss. Wenn Sie "Dirty Reads" zulassen, müssen Sie möglicherweise auf serialisierbar stoßen.

Duffymo
quelle
Ich denke, sie meinten Hashtable. Es wurde als Teil von JDK 1.0 ausgeliefert. Wie Vector wurde es so geschrieben, dass es threadsicher und langsam ist. Beide wurden durch nicht threadsichere Alternativen ersetzt: HashMap und ArrayList. Zahlen Sie für das, was Sie verwenden.
Duffymo
0

Probieren Sie entweder CopyOnWriteArrayList oder CopyOnWriteArraySet aus, je nachdem, was Sie versuchen.

Javamann
quelle
0

Beachten Sie, dass die ausgewählte Antwort nicht direkt vor einer Änderung auf Ihren Kontext angewendet werden kann, wenn Sie versuchen, einige Einträge aus der Karte zu entfernen, während Sie die Karte wie ich durchlaufen.

Ich gebe hier nur mein Arbeitsbeispiel für Neulinge, um ihre Zeit zu sparen:

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
quelle
0

Ich bin auf diese Ausnahme gestoßen, als ich versucht habe, x letzte Elemente aus der Liste zu entfernen. myList.subList(lastIndex, myList.size()).clear();war die einzige Lösung, die für mich funktioniert hat.

grauer Mann
quelle