Почему выбрасывается исключение ConcurrentModificationException и как его отлаживать
Я использую Collection
(HashMap
используется косвенно JPA, так бывает), но, очевидно, случайным образом код генерирует 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());
}
Это вызовет исключение ConcurrentModificationException, когда it.hasNext() вызывается во второй раз.
Правильный подход будет
Iterator it = map.entrySet().iterator();
while (it.hasNext())
{
Entry item = it.next();
it.remove();
}
Предполагая, что этот итератор поддерживает операцию remove().
Ответ 2
Попробуйте использовать ConcurrentHashMap вместо простой HashMap
Ответ 3
Модификация Collection
во время итерации по этой Collection
с использованием Iterator
не разрешена большинством классов Collection
. Библиотека Java называет попытку изменить Collection
, повторяя ее, "одновременной модификацией", которая, к сожалению, предполагает единственно возможную причину - одновременную модификацию несколькими потоками, но это не так. Используя только один поток, можно создать итератор для Collection
(используя Collection.iterator()
или расширенный цикл for
), запустить итерацию (используя Iterator.next()
или эквивалентно ввести тело расширенного цикла for
), измените Collection
, затем продолжите итерацию.
Чтобы помочь программистам, некоторые реализации этих классов Collection
пытаются обнаружить ошибочную одновременную модификацию и выдают ConcurrentModificationException
если они его обнаруживают. Однако, как правило, невозможно и практически невозможно гарантировать обнаружение всех одновременных изменений. Поэтому ошибочное использование Collection
не всегда приводит к исключению ConcurrentModificationException
.
Документация ConcurrentModificationException
гласит:
Это исключение может быть вызвано методами, которые обнаружили одновременную модификацию объекта, когда такая модификация недопустима...
Обратите внимание, что это исключение не всегда указывает на то, что объект был одновременно изменен другим потоком. Если один поток выдает последовательность вызовов методов, которая нарушает контракт объекта, объект может вызвать это исключение...
Обратите внимание, что отказоустойчивое поведение не может быть гарантировано, так как, вообще говоря, невозможно сделать какие-либо жесткие гарантии при наличии несинхронизированной параллельной модификации. Отказоустойчивые операции создают ConcurrentModificationException
ситуацию ConcurrentModificationException
.
Обратите внимание, что
Документация классов HashSet
, HashMap
, TreeSet
и ArrayList
гласит:
Итераторы, возвращенные [прямо или косвенно из этого класса], не подвержены сбоям: если [коллекция] изменяется в любое время после создания итератора, любым способом, кроме как через собственный метод удаления итератора, итератор Iterator
исключение ConcurrentModificationException
. Таким образом, перед одновременной модификацией итератор быстро и чисто дает сбой, вместо того, чтобы рисковать произвольным недетерминированным поведением в неопределенное время в будущем.
Обратите внимание, что отказоустойчивое поведение итератора не может быть гарантировано, так как, вообще говоря, невозможно сделать какие-либо жесткие гарантии при наличии несинхронизированной параллельной модификации. Отказоустойчивые итераторы создают ConcurrentModificationException
ситуацию ConcurrentModificationException
. Следовательно, было бы неправильно писать программу, которая зависела от этого исключения в отношении его корректности: поведение итераторов, обеспечивающее отказоустойчивость, следует использовать только для обнаружения ошибок.
Еще раз обратите внимание, что поведение "не может быть гарантировано" и только "на основе максимальных усилий".
Документация нескольких методов интерфейса Map
говорит это:
Неконкурентные реализации должны переопределить этот метод и, с максимальной отдачей, генерировать ConcurrentModificationException
если обнаружено, что функция отображения изменяет эту карту во время вычисления. Параллельные реализации должны переопределить этот метод и, с максимальной IllegalStateException
, генерировать IllegalStateException
если обнаружено, что функция отображения изменяет эту карту во время вычисления и, как результат, вычисление никогда не завершится.
Еще раз обратите внимание, что для обнаружения требуется только "основа наилучшего возможного", а ConcurrentModificationException
явно предлагается только для не параллельных (не ориентированных на потоки) классов.
Отладка ConcurrentModificationException
Таким образом, когда вы видите трассировку стека из-за ConcurrentModificationException
, вы не можете сразу предположить, что причиной является небезопасный многопоточный доступ к Collection
. Необходимо проверить трассировку стека, чтобы определить, какой класс Collection
сгенерировал исключение (метод класса прямо или косвенно его сгенерировал) и для какого объекта Collection
. Затем вы должны проверить, откуда этот объект может быть изменен.
- Наиболее распространенной причиной является изменение
Collection
в расширенном цикле for
над Collection
. То, что вы не видите объект Iterator
в исходном коде, не означает, что там нет Iterator
! К счастью, один из операторов ошибочного цикла for
обычно находится в трассировке стека, поэтому отследить ошибку обычно легко. - Более сложный случай - когда ваш код передает ссылки на объект
Collection
. Обратите внимание, что неизменяемые представления коллекций (например, созданные Collections.unmodifiableList()
) сохраняют ссылку на изменяемую коллекцию, поэтому итерация над "неизменяемой" коллекцией может вызвать исключение (модификация была сделана в другом месте). Другие виды вашей Collection
, такие как подсписков, Map
входных наборов и Map
наборов ключей также сохраняют ссылки на оригинал (изменяемый) Collection
. Это может быть проблемой даже для многопоточной Collection
, такой как CopyOnWriteList
; не предполагайте, что поточно-ориентированные (одновременные) сборы никогда не могут вызвать исключение. - Какие операции могут изменить
Collection
может быть неожиданным в некоторых случаях. Например, LinkedHashMap.get()
изменяет свою коллекцию. - Самые тяжелые случаи, когда исключение происходит из-за одновременной модификации несколькими потоками.
Программирование для предотвращения одновременных ошибок модификации
По возможности ограничивайте все ссылки на объект Collection
, чтобы было легче предотвратить одновременные изменения. Сделайте Collection
private
объектом или локальной переменной и не возвращайте ссылки на Collection
или ее итераторы из методов. Тогда намного легче исследовать все места, где Collection
может быть изменена. Если Collection
должна использоваться несколькими потоками, тогда практично обеспечить, чтобы потоки обращались к Collection
только с соответствующей синхронизацией и блокировкой.
Ответ 4
Это звучит не так, как проблема синхронизации с Java, и больше похоже на проблему блокировки базы данных.
Я не знаю, будет ли добавление версии ко всем вашим постоянным классам, но так, что Hibernate может обеспечить эксклюзивный доступ к строкам в таблице.
Возможно, уровень изоляции должен быть выше. Если вы разрешаете "грязные чтения", возможно, вам нужно поддаться сериализации.
Ответ 5
Попробуйте либо CopyOnWriteArrayList, либо CopyOnWriteArraySet в зависимости от того, что вы пытаетесь сделать.
Ответ 6
Обратите внимание, что выбранный ответ не может быть применен к вашему контексту непосредственно перед некоторой модификацией, если вы пытаетесь удалить некоторые записи с карты, выполняя итерацию карты точно так же, как я.
Я просто привожу свой рабочий пример, чтобы новички сэкономили свое время:
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();
}