Почему выбрасывается исключение 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();
    }