Dlaczego został zgłoszony wyjątek ConcurrentModificationException i jak go debugować

130

Używam Collection(a HashMapużywany pośrednio przez JPA, tak się dzieje), ale najwyraźniej losowo kod rzuca ConcurrentModificationException. Co to powoduje i jak rozwiązać ten problem? Może używając jakiejś synchronizacji?

Oto pełny ślad stosu:

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
źródło
1
Czy możesz podać więcej kontekstu? Łączysz, aktualizujesz lub usuwasz podmiot? Jakie skojarzenia ma ten podmiot? A co z ustawieniami kaskadowymi?
ordnungswidrig
1
Na podstawie śladu stosu widać, że wyjątek występuje podczas iteracji w HashMap. Z pewnością jakiś inny wątek modyfikuje mapę, ale wyjątek występuje w wątku, który iteruje.
Chochos

Odpowiedzi:

263

To nie jest problem z synchronizacją. Nastąpi to, jeśli podstawowa kolekcja, po której jest iterowana, zostanie zmodyfikowana przez coś innego niż sam iterator.

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

Spowoduje to wyświetlenie znaku, ConcurrentModificationExceptiongdy it.hasNext()zostanie wywołany po raz drugi.

Właściwe byłoby podejście

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

Zakładając, że ten iterator obsługuje remove()operację.

Rudzik
źródło
1
Możliwe, ale wygląda na to, że Hibernate wykonuje iterację, która powinna zostać zaimplementowana w miarę poprawnie. Może wystąpić wywołanie zwrotne modyfikujące mapę, ale jest to mało prawdopodobne. Nieprzewidywalność wskazuje na rzeczywisty problem ze współbieżnością.
Tom Hawtin - tackline
Ten wyjątek nie ma nic wspólnego z współbieżnością wątków, jest on spowodowany magazynem zapasowym modyfikowanego iteratora. Czy przez inny wątek nie ma znaczenia dla iteratora. IMHO to źle nazwany wyjątek, ponieważ daje błędne wyobrażenie o przyczynie.
Robin
Zgadzam się jednak, że jeśli jest to nieprzewidywalne, najprawdopodobniej występuje problem z wątkami, który powoduje spełnienie warunków tego wyjątku. Co sprawia, że ​​jest to tym bardziej zagmatwane ze względu na nazwę wyjątku.
Robin
To jest poprawne i lepsze wyjaśnienie niż zaakceptowana odpowiedź, ale zaakceptowana odpowiedź to fajna poprawka. ConcurrentHashMap nie podlega CME, nawet wewnątrz iteratora (chociaż iterator jest nadal zaprojektowany dla dostępu jednowątkowego).
G__
To rozwiązanie nie ma sensu, ponieważ Mapy nie mają metody iterator (). Przykład Robina miałby zastosowanie np. Do list.
Piotr
72

Spróbuj użyć ConcurrentHashMapzamiast zwykłegoHashMap

Chochos
źródło
Czy to naprawdę rozwiązało problem? Mam ten sam problem, ale z całą pewnością mogę wykluczyć jakiekolwiek problemy z wątkami.
tobiasbayer
5
Innym rozwiązaniem jest utworzenie kopii mapy i zamiast tego iteracyjne przeglądanie tej kopii. Lub skopiuj zestaw kluczy i iteruj po nich, uzyskując wartość dla każdego klucza z oryginalnej mapy.
Chochos
To Hibernate wykonuje iterację w kolekcji, więc nie można jej po prostu skopiować.
tobiasbayer
1
Natychmiastowy wybawca. Sprawdzę, dlaczego to zadziałało tak dobrze, więc nie dostaję więcej niespodzianek w dalszej części drogi.
Valchris
1
Myślę, że to problem z synchronizacją to problem, jeśli modyfikacja tej samej modyfikacji podczas zapętlania tego samego obiektu.
Rais Alam
17

Modyfikacja czasu Collectionpodczas iteracji przez to Collectionprzy użyciu an nieIterator jest dozwolona przez większość Collectionklas. Biblioteka Javy wywołuje próbę zmodyfikowania Collectionpodczas iteracji przez nią „współbieżną modyfikacją”. To niestety sugeruje, że jedyną możliwą przyczyną jest jednoczesna modyfikacja przez wiele wątków, ale tak nie jest. Używając tylko jednego wątku, można utworzyć iterator dla Collection(używając Collection.iterator()lub rozszerzonej forpętli ), rozpocząć iterację (używając Iterator.next()lub równoważnie wprowadzając treść rozszerzonej forpętli), zmodyfikować Collection, a następnie kontynuować iterację.

Aby pomóc programistom, niektóre implementacje tych Collectionklas próbują wykryć błędną, współbieżną modyfikację i wyrzucić, ConcurrentModificationExceptionjeśli ją wykryją. Jednak generalnie nie jest możliwe i praktyczne zagwarantowanie wykrywania wszystkich jednoczesnych modyfikacji. Tak więc błędne użycie Collectionnie zawsze kończy się rzutem ConcurrentModificationException.

Dokumentacja ConcurrentModificationExceptionmówi:

Ten wyjątek może zostać zgłoszony przez metody, które wykryły równoczesną modyfikację obiektu, gdy taka modyfikacja jest niedozwolona ...

Należy zauważyć, że ten wyjątek nie zawsze oznacza, że ​​obiekt został jednocześnie zmodyfikowany przez inny wątek. Jeśli pojedynczy wątek wydaje sekwencję wywołań metod, która narusza kontrakt obiektu, obiekt może zgłosić ten wyjątek ...

Należy zauważyć, że nie można zagwarantować zachowania bezawaryjnego, ponieważ, ogólnie rzecz biorąc, niemożliwe jest zapewnienie jakichkolwiek twardych gwarancji w przypadku niezsynchronizowanej, równoczesnej modyfikacji. Operacje bezawaryjne wymagają ConcurrentModificationExceptionnajlepszych starań.

Zwróć na to uwagę

Dokumentacja HashSet, HashMap, TreeSeti ArrayListzajęcia mówi w ten sposób:

Iteratory zwrócone [bezpośrednio lub pośrednio z tej klasy] działają bezawaryjnie: jeśli [kolekcja] zostanie zmodyfikowana w dowolnym momencie po utworzeniu iteratora, w jakikolwiek sposób z wyjątkiem metody usuwania iteratora, IteratorgenerujeConcurrentModificationException . Zatem w obliczu równoczesnej modyfikacji iterator zawodzi szybko i czysto, zamiast ryzykować arbitralne, niedeterministyczne zachowanie w nieokreślonym czasie w przyszłości.

Należy zauważyć, że nie można zagwarantować szybkiego zachowania iteratora w przypadku awarii, ponieważ, ogólnie rzecz biorąc, niemożliwe jest zapewnienie jakichkolwiek twardych gwarancji w przypadku niezsynchronizowanej współbieżnej modyfikacji. Iteratory działające bezawaryjnie wykonują wszystko, ConcurrentModificationExceptionco najlepsze. Dlatego błędem byłoby napisanie programu, który zależałby od tego wyjątku ze względu na jego poprawność: szybkie zachowanie iteratorów w przypadku awarii powinno być używane tylko do wykrywania błędów .

Zauważ jeszcze raz, że zachowanie „nie może być zagwarantowane” i jest tylko „na zasadzie najlepszych starań”.

Dokumentacja kilku metod Mapinterfejsu mówi tak:

Implementacje niewspółbieżne powinny przesłonić tę metodę i, na zasadzie najlepszego wysiłku, zgłosić a, ConcurrentModificationExceptionjeśli zostanie wykryte, że funkcja mapowania modyfikuje tę mapę podczas obliczeń. Współbieżne implementacje powinny przesłonić tę metodę i, na zasadzie najlepszego wysiłku, zgłosić, IllegalStateExceptionjeśli zostanie wykryte, że funkcja mapowania modyfikuje tę mapę podczas obliczeń, w wyniku czego obliczenia nigdy się nie zakończą.

Zwróć uwagę, że do wykrywania wymagana jest tylko „podstawa najlepszego wysiłku”, a a ConcurrentModificationExceptionjest wyraźnie sugerowane tylko dla klas innych niż współbieżne (niegwintowane).

Debugowanie ConcurrentModificationException

Tak więc, gdy widzisz ślad stosu związany z a ConcurrentModificationException, nie możesz od razu założyć, że przyczyną jest niebezpieczny wielowątkowy dostęp do pliku Collection. Musisz zbadać ślad stosu, aby określić, która klasa Collectionwyrzuciła wyjątek (metoda klasy bezpośrednio lub pośrednio go wyrzuci) i dla którego Collectionobiektu. Następnie musisz zbadać, skąd ten obiekt może być modyfikowany.

  • Najczęstszą przyczyną jest modyfikacja Collectionwewnątrz rozszerzonej forpętli na Collection. To, że nie widzisz Iteratorobiektu w kodzie źródłowym, nie oznacza, że ​​go nie Iteratorma! Na szczęście jedno z instrukcji błędnej forpętli będzie zwykle znajdować się w śladzie stosu, więc śledzenie błędu jest zwykle łatwe.
  • Trudniejszy przypadek ma miejsce, gdy kod przekazuje odwołania do Collection obiektu. Należy zauważyć, że niemodyfikowalne widoki kolekcji (na przykład utworzone przez Collections.unmodifiableList()) zachowują odniesienie do modyfikowalnej kolekcji, więc iteracja po kolekcji „niemodyfikowalnej” może zgłosić wyjątek (modyfikacja została przeprowadzona gdzie indziej). Inne poglądy twoich Collection, takie jak list sub , Mapzestawy wejścia i Mapnajważniejszych zbiorów zachowują również odniesienia do oryginału (modyfikacji) Collection. Może to stanowić problem nawet dla bezpiecznego wątku Collection, takiego jak CopyOnWriteList; Nie zakładaj, że kolekcje bezpieczne wątkowo (współbieżne) nigdy nie mogą zgłosić wyjątku.
  • To, które operacje mogą modyfikować, Collectionmoże być w niektórych przypadkach nieoczekiwane. Na przykład LinkedHashMap.get()modyfikuje swoją kolekcję .
  • Najtrudniejsze przypadki, gdy wyjątek jest w wyniku jednoczesnej modyfikacji przez wielu wątków.

Programowanie, aby zapobiec jednoczesnym błędom modyfikacji

Jeśli to możliwe, ogranicz wszystkie odwołania do Collectionobiektu, aby łatwiej było zapobiec równoczesnym modyfikacjom. Dokonać Collectiondo privateobiektu lub zmiennej lokalnej, a nie powrót do odwołania Collectionlub jego iteratorów z metod. Dużo łatwiej jest wtedy zbadać wszystkie miejsca, w których Collectionmożna zmodyfikować. Jeśli Collectionma być używany przez wiele wątków, praktyczne jest upewnienie się, że wątki mają dostęp Collectiontylko do tych z odpowiednią synchronizacją i blokowaniem.

Raedwald
źródło
Zastanawiam się, dlaczego jednoczesna modyfikacja nie jest dozwolona w przypadku pojedynczego wątku. Jakie problemy mogą wystąpić, jeśli pojedynczy wątek może wykonać równoczesną modyfikację na zwykłej mapie skrótów?
MasterJoe
4

W Javie 8 możesz użyć wyrażenia lambda:

map.keySet().removeIf(key -> key condition);
Zentopia
źródło
2

Brzmi to mniej jak problem z synchronizacją Java, a bardziej jak problem z blokowaniem bazy danych.

Nie wiem, czy dodanie wersji do wszystkich twoich trwałych klas rozwiąże ten problem, ale to jeden ze sposobów, w jaki Hibernate może zapewnić wyłączny dostęp do wierszy w tabeli.

Możliwe, że poziom izolacji musi być wyższy. Jeśli zezwolisz na „brudne odczyty”, być może będziesz musiał zwiększyć możliwości serializacji.

duffymo
źródło
Myślę, że mieli na myśli Hashtable. Został dostarczony jako część JDK 1.0. Podobnie jak Vector, został napisany, aby był bezpieczny dla wątków - i wolny. Oba zostały zastąpione alternatywami bezpiecznymi dla wątków: HashMap i ArrayList. Płać za to, czego używasz.
duffymo
0

Spróbuj CopyOnWriteArrayList lub CopyOnWriteArraySet w zależności od tego, co próbujesz zrobić.

Javamann
źródło
0

Zauważ, że wybrana odpowiedź nie może być zastosowana do twojego kontekstu bezpośrednio przed jakąś modyfikacją, jeśli próbujesz usunąć niektóre wpisy z mapy podczas iteracji mapy, tak jak ja.

Podaję tutaj mój przykład roboczy dla początkujących, aby zaoszczędzić czas:

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
źródło
0

Wpadłem na ten wyjątek podczas próby usunięcia x ostatnich pozycji z listy. myList.subList(lastIndex, myList.size()).clear();było jedynym rozwiązaniem, które działało dla mnie.

szary człowiek
źródło