Вопрос-ответ

Why is a ConcurrentModificationException thrown and how to debug it

Почему возникает ConcurrentModificationException и как его отладить

Я использую a Collection (так получилось, что a HashMap косвенно используется JPA), но, по-видимому, случайным образом код выдает a ConcurrentModificationException. Что является его причиной и как мне решить эту проблему? Возможно, с помощью некоторой синхронизации?

Вот полная трассировка стека:

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

Это не проблема синхронизации. Это произойдет, если базовая коллекция, по которой выполняется итерация, изменена чем-либо иным, кроме самого итератора.

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

Это вызовет a ConcurrentModificationException при it.hasNext() вызове во второй раз.

Правильным подходом было бы

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

Предполагая, что этот итератор поддерживает remove() операцию.

Ответ 2

Попробуйте использовать ConcurrentHashMap вместо простого HashMap

Ответ 3

Модификация a Collection во время итерации через это, Collection используя an Iterator, не разрешена большинством Collection классов. Библиотека Java вызывает попытку изменить Collection во время итерации по нему "параллельную модификацию". Это, к сожалению, предполагает, что единственной возможной причиной является одновременная модификация несколькими потоками, но это не так. Используя только один поток, можно создать итератор для Collection (используя Collection.iterator() или расширенный for цикл), начать итерацию (используя Iterator.next() или, что эквивалентно, введя тело расширенного for цикла), изменить Collection, затем продолжить итерацию.

Чтобы помочь программистам, некоторые реализации этих Collection классов пытаются обнаружить ошибочную параллельную модификацию и выдают a ConcurrentModificationException, если они ее обнаруживают. Однако, как правило, гарантировать обнаружение всех одновременных модификаций невозможно и непрактично. Таким образом, ошибочное использование Collection не всегда приводит к сбросу ConcurrentModificationException.

В документации ConcurrentModificationException говорится:


Это исключение может быть вызвано методами, которые обнаружили одновременную модификацию объекта, когда такая модификация недопустима...


Обратите внимание, что это исключение не всегда указывает на то, что объект был одновременно изменен другим потоком. Если один поток выдает последовательность вызовов метода, которая нарушает контракт объекта, объект может выдать это исключение...


Обратите внимание, что отказоустойчивое поведение не может быть гарантировано, поскольку, вообще говоря, невозможно дать какие-либо твердые гарантии при наличии несинхронизированной параллельной модификации. Отказоустойчивые операции приводят к сбою ConcurrentModificationException при максимальных усилиях.


Обратите внимание, что


  • исключение может быть throw, а не должно быть throw

  • разные потоки не требуются

  • создание исключения не может быть гарантировано

  • создание исключения осуществляется на основе максимальных усилий

  • исключение возникает при обнаружении, а не при ее возникновении

В документации к классам HashSet, HashMap, TreeSet и ArrayList об этом говорится:


Итераторы, возвращаемые [прямо или косвенно из этого класса], быстры в отказоустойчивости: если [коллекция] модифицируется в любое время после создания итератора любым способом, кроме как через собственный метод remove итератора, Iterator выдает ConcurrentModificationException. Таким образом, при одновременном изменении итератор завершается сбоем быстро и чисто, вместо того, чтобы рисковать произвольным, недетерминированным поведением в неопределенное время в будущем.


Обратите внимание, что отказоустойчивое поведение итератора не может быть гарантировано, поскольку, вообще говоря, невозможно дать какие-либо твердые гарантии при наличии несинхронизированной параллельной модификации. Итераторы с отказоустойчивостью запускают ConcurrentModificationException на основе максимальных усилий. Следовательно, было бы неправильно писать программу, корректность которой зависела бы от этого исключения: отказоустойчивое поведение итераторов следует использовать только для обнаружения ошибок.


Еще раз обратите внимание, что поведение "не может быть гарантировано" и выполняется только "на основе максимальных усилий".

В документации нескольких методов Map интерфейса говорится об этом:


Неконкурентные реализации должны переопределять этот метод и, по возможности, выдавать a ConcurrentModificationException если обнаружено, что функция сопоставления изменяет эту карту во время вычисления. Параллельные реализации должны переопределять этот метод и, исходя из максимальных усилий, выдавать IllegalStateException если обнаружено, что функция сопоставления изменяет эту карту во время вычисления, и в результате вычисление никогда не завершится.


Еще раз обратите внимание, что для обнаружения требуется только "наилучшая основа", а ConcurrentModificationException явно предлагается только для неконкурентных (не потокобезопасных) классов.

Отладка ConcurrentModificationException

Итак, когда вы видите трассировку стека из-за a ConcurrentModificationException, вы не можете сразу предположить, что причиной является небезопасный многопоточный доступ к a Collection. Вы должны изучить трассировку стека, чтобы определить, какой класс Collection выдал исключение (метод класса прямо или косвенно выдал его) и для какого Collection объекта. Затем вы должны изучить, откуда этот объект может быть изменен.


  • Наиболее распространенной причиной является модификация Collection в расширенном for цикле поверх Collection. То, что вы не видите Iterator объект в своем исходном коде, не означает, что его там нет Iterator! К счастью, один из операторов неисправного for цикла обычно находится в трассировке стека, поэтому отследить ошибку обычно несложно.

  • Более сложный случай - это когда ваш код передает ссылки на Collection объект. Обратите внимание, что неизменяемые представления коллекций (например, созданные Collections.unmodifiableList()) сохраняют ссылку на изменяемую коллекцию, поэтому итерация по "неизменяемой" коллекции может вызвать исключение (модификация была выполнена в другом месте). Другие ваши представления Collection, такие как вложенные списки, Map наборы записей и Map наборы ключей, также сохраняют ссылки на оригинал (изменяемый) Collection. Это может быть проблемой даже для потокобезопасных Collection, таких как CopyOnWriteList; не предполагайте, что потокобезопасные (параллельные) коллекции никогда не смогут выдавать исключение.

  • Какие операции могут изменять Collection в некоторых случаях может быть неожиданным. Например, LinkedHashMap.get() изменяет его коллекцию.

  • Самые сложные случаи - это когда исключение возникает из-за одновременной модификации несколькими потоками.

Программирование для предотвращения ошибок одновременного изменения

По возможности ограничивайте все ссылки на Collection объект, чтобы было проще предотвратить одновременные модификации. Создайте объект Collection a private или локальную переменную и не возвращайте ссылки на Collection или его итераторы из методов. Тогда намного проще изучить все места, где Collection может быть изменено. Если Collection должен использоваться несколькими потоками, тогда целесообразно гарантировать, что потоки обращаются к Collection только с соответствующей синхронизацией и блокировкой.

Ответ 4

В Java 8 вы можете использовать лямбда-выражение:

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

removeIf это удобный default метод в Collection, который использует Iterator внутренний перебор элементов вызывающей коллекции.
Извлечение условия удаления выражается в разрешении вызывающей стороне предоставить Predicate<? super E>.

"Я выполню итерацию за вас и протестирую ваш Predicate на каждом из элементов коллекции. Если элемент вызывает возврат test метода Predicate true, я удалю его."

java exception collections