Perché viene generata una ConcurrentModificationException e come eseguirne il debug


130

Sto usando un Collection(un HashMapusato indirettamente dall'APP, succede così), ma a quanto pare casualmente il codice genera un ConcurrentModificationException. Cosa lo sta causando e come posso risolvere questo problema? Usando un po 'di sincronizzazione, forse?

Ecco la traccia stack completa:

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)

1
Potete fornire qualche altro contesto? Stai unendo, aggiornando o eliminando un'entità? Quali associazioni hanno questa entità? E le tue impostazioni a cascata?
ordnungswidrig,

1
Dalla traccia dello stack è possibile vedere che si verifica l'eccezione durante l'iterazione tramite HashMap. Sicuramente qualche altro thread sta modificando la mappa ma l'eccezione si verifica nel thread che sta iterando.
Chochos,

Risposte:


263

Questo non è un problema di sincronizzazione. Ciò si verificherà se la raccolta sottostante su cui si esegue l'iterazione viene modificata da qualcosa di diverso dall'Iteratore stesso.

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

Questo lancerà a ConcurrentModificationException quando it.hasNext()viene chiamato la seconda volta.

L'approccio corretto sarebbe

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

Supponendo che questo iteratore supporti l' remove()operazione.


1
Forse, ma sembra che Hibernate stia eseguendo l'iterazione, che dovrebbe essere implementata ragionevolmente correttamente. Potrebbe esserci un callback che modifica la mappa, ma è improbabile. L'imprevedibilità indica un vero problema di concorrenza.
Tom Hawtin - tackline

Questa eccezione non ha nulla a che fare con la concorrenza di threading, è causata dalla modifica dell'archivio di backup dell'iteratore. Se da un altro thread di non importa all'iteratore. IMHO è un'eccezione mal denominata poiché dà un'impressione errata della causa.
Robin

Concordo tuttavia sul fatto che se è imprevedibile, è molto probabile che vi sia un problema di threading che sta causando le condizioni per questa eccezione. Il che rende ancora più confuso a causa del nome dell'eccezione.
Robin

Questa è una spiegazione corretta e migliore della risposta accettata, ma la risposta accettata è una buona soluzione. ConcurrentHashMap non è soggetto a CME, nemmeno all'interno di un iteratore (sebbene l'iteratore sia ancora progettato per l'accesso a thread singolo).
G__

Questa soluzione non ha senso, perché Maps non ha il metodo iterator (). L'esempio di Robin sarebbe applicabile ad esempio alle liste.
Pietro,

72

Prova a usare un ConcurrentHashMapanziché un sempliceHashMap


Questo ha davvero risolto il problema? Sto riscontrando lo stesso problema, ma posso sicuramente escludere eventuali problemi di threading.
Tobiasbayer,

5
Un'altra soluzione è creare una copia della mappa e scorrere invece quella copia. Oppure copia il set di chiavi e esegui l'iterazione attraverso di esse, ottenendo il valore per ciascuna chiave dalla mappa originale.
Chochos,

È Hibernate a scorrere la raccolta, quindi non puoi semplicemente copiarla.
Tobiasbayer,

1
Salvatore istantaneo. Andando a capire perché questo ha funzionato così bene, quindi non ho più sorprese più avanti.
Valchris,

1
Immagino che non sia un problema di sincronizzazione, è un problema se la modifica della stessa modifica durante il loop dello stesso oggetto.
Rais Alam,

17

La modifica di un Collectionpo 'che scorre attraverso l' Collectionuso di un nonIterator è consentita dalla maggior parte delle Collectionclassi. La libreria Java chiama un tentativo di modifica Collectionmentre esegue ripetutamente una "modifica simultanea". Ciò sfortunatamente suggerisce che l'unica causa possibile è la modifica simultanea di più thread, ma non è così. Utilizzando un solo thread è possibile creare un iteratore per Collection(usando Collection.iterator(), o un ciclo avanzatofor ), iniziare iterando (usando Iterator.next()o inserendo in modo equivalente il corpo del forciclo migliorato ), modificare Collection, quindi continuare iterando.

Per aiutare i programmatori, alcune implementazioni di quelle Collectionclassi tentano di rilevare modifiche simultanee errate e lanciano un ConcurrentModificationExceptionse la rilevano. Tuttavia, in generale non è possibile e pratico garantire il rilevamento di tutte le modifiche simultanee. Quindi un uso errato del Collectionnon sempre provoca un lancio ConcurrentModificationException.

La documentazione di ConcurrentModificationExceptiondice:

Questa eccezione può essere generata da metodi che hanno rilevato modifiche simultanee di un oggetto quando tale modifica non è consentita ...

Si noti che questa eccezione non indica sempre che un oggetto è stato modificato contemporaneamente da un thread diverso. Se un singolo thread emette una sequenza di invocazioni di metodi che violano il contratto di un oggetto, l'oggetto può generare questa eccezione ...

Si noti che il comportamento fail-fast non può essere garantito in quanto, in generale, è impossibile fornire garanzie concrete in presenza di modifiche simultanee non sincronizzate. Operazioni fail-fast lanciate ConcurrentModificationExceptionsulla base del massimo sforzo.

Nota che

La documentazione del HashSet, HashMap, TreeSete ArrayListle classi dice questo:

Gli iteratori restituiti [direttamente o indirettamente da questa classe] sono fail-fast: se la [raccolta] viene modificata in qualsiasi momento dopo la creazione dell'iteratore, in qualsiasi modo, tranne che attraverso il metodo di rimozione dell'iteratore stesso, i Iteratorlanci a ConcurrentModificationException. Pertanto, di fronte a modifiche simultanee, l'iteratore fallisce in modo rapido e pulito, piuttosto che rischiare un comportamento arbitrario e non deterministico in un momento indeterminato in futuro.

Si noti che il comportamento fail-fast di un iteratore non può essere garantito in quanto, in generale, è impossibile fornire garanzie concrete in presenza di modifiche simultanee non sincronizzate. Gli iteratori fail-fast vengono lanciati ConcurrentModificationExceptionsulla base del massimo sforzo. Pertanto, sarebbe errato scrivere un programma che dipendesse da questa eccezione per la sua correttezza: il comportamento fail-fast degli iteratori dovrebbe essere usato solo per rilevare i bug .

Si noti ancora che il comportamento "non può essere garantito" ed è "solo sulla base del massimo sforzo".

La documentazione di diversi metodi Mapdell'interfaccia dice questo:

Le implementazioni non simultanee dovrebbero sovrascrivere questo metodo e, nel migliore dei modi, lanciare un ConcurrentModificationExceptionse viene rilevato che la funzione di mappatura modifica questa mappa durante il calcolo. Le implementazioni simultanee dovrebbero sovrascrivere questo metodo e, nel migliore dei modi, lanciare un IllegalStateExceptionse viene rilevato che la funzione di mappatura modifica questa mappa durante il calcolo e di conseguenza il calcolo non si completerebbe mai.

Notare di nuovo che per il rilevamento è necessaria solo una "base del massimo sforzo" e una ConcurrentModificationExceptionè esplicitamente suggerita solo per le classi non concorrenti (non thread-safe).

Debug ConcurrentModificationException

Quindi, quando vedi una traccia dello stack dovuta a ConcurrentModificationException, non puoi immediatamente supporre che la causa sia un accesso multi-thread non sicuro a Collection. È necessario esaminare lo stack-trace per determinare quale classe di Collectionha generato l'eccezione (un metodo della classe l'avrà lanciata direttamente o indirettamente) e per quale Collectionoggetto. Quindi è necessario esaminare da dove l'oggetto può essere modificato.

  • La causa più comune è la modifica di Collectionall'interno di un forloop avanzato rispetto a Collection. Solo perché non vedi un Iteratoroggetto nel tuo codice sorgente non significa che non Iteratorci sia! Fortunatamente, una delle affermazioni del forloop difettoso di solito si trova nello stack-trace, quindi di solito è facile rintracciare l'errore.
  • Un caso più complicato è quando il codice passa attorno ai riferimenti Collectionall'oggetto. Si noti che le viste non modificabili delle raccolte (come quelle prodotte da Collections.unmodifiableList()) conservano un riferimento alla raccolta modificabile, quindi l' iterazione su una raccolta "non modificabile" può generare l'eccezione (la modifica è stata apportata altrove). Altre vista della vostra Collection, come ad esempio le liste sub , Mapset di entrata e Mapset di chiavi mantengono anche riferimenti a quello originale (modificabile) Collection. Questo può essere un problema anche per un thread-safe Collection, comeCopyOnWriteList ; non dare per scontato che le raccolte thread-safe (simultanee) non possano mai generare l'eccezione.
  • Quali operazioni possono modificare a Collectionpossono essere inattese in alcuni casi. Per esempio,LinkedHashMap.get() modifica la sua raccolta .
  • I casi più difficili si verificano quando l'eccezione è dovuta a modifiche simultanee da parte di più thread.

Programmazione per prevenire errori di modifica simultanei

Quando possibile, limitare tutti i riferimenti a un Collectionoggetto, quindi è più facile prevenire modifiche simultanee. Crea Collectionun privateoggetto o una variabile locale e non restituire riferimenti ai Collectionsuoi iteratori dai metodi. È quindi molto più semplice esaminare tutti i luoghi in cui è Collectionpossibile modificarli. Se Collectiondeve essere utilizzato da più thread, è quindi pratico assicurarsi che i thread accedano Collectionall'unico con sincronizzazione e blocco appropriati.


Mi chiedo perché non sia consentita la modifica simultanea nel caso di un singolo thread. Quali problemi possono verificarsi se a un singolo thread è consentito apportare una modifica simultanea su una normale mappa hash?
MasterJoe

4

In Java 8, puoi usare l'espressione lambda:

map.keySet().removeIf(key -> key condition);

2

Sembra meno un problema di sincronizzazione Java e più un problema di blocco del database.

Non so se l'aggiunta di una versione a tutte le classi persistenti la risolverà, ma è un modo in cui Hibernate può fornire accesso esclusivo alle righe in una tabella.

Potrebbe essere che il livello di isolamento debba essere più alto. Se si consentono "letture sporche", forse è necessario passare alla serializzazione.


Penso che intendessero Hashtable. È stato spedito come parte di JDK 1.0. Come Vector, è stato scritto per essere thread-safe - e lento. Entrambi sono stati sostituiti da alternative non thread-safe: HashMap e ArrayList. Paga per quello che usi.
duffymo,

0

Prova CopyOnWriteArrayList o CopyOnWriteArraySet a seconda di ciò che stai cercando di fare.


0

Nota che la risposta selezionata non può essere applicata al tuo contesto direttamente prima di alcune modifiche, se stai provando a rimuovere alcune voci dalla mappa mentre esegui l'iterazione della mappa proprio come me.

Do solo il mio esempio di lavoro qui per i neofiti per risparmiare tempo:

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

0

Mi sono imbattuto in questa eccezione quando ho provato a rimuovere x ultimi elementi dall'elenco. myList.subList(lastIndex, myList.size()).clear();è stata l'unica soluzione che ha funzionato per me.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.