Почему возникает 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
, я удалю его."