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

130

Я использую 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)
mainstringargs
источник
1
Не могли бы вы предоставить дополнительный контекст? Вы объединяете, обновляете или удаляете объект? Какие ассоциации есть у этого объекта? А как насчет ваших настроек каскадирования?
ordnungswidrig
1
Из трассировки стека вы можете видеть, что исключение происходит во время итерации через HashMap. Конечно, какой-то другой поток изменяет карту, но исключение возникает в потоке, который выполняет итерацию.
Chochos

Ответы:

263

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

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()операцию.

Робин
источник
1
Возможно, но похоже, что Hibernate выполняет итерацию, которая должна быть реализована разумно правильно. При изменении карты может быть обратный вызов, но это маловероятно. Непредсказуемость указывает на реальную проблему параллелизма.
Том Хотин - tackline 02
Это исключение не имеет ничего общего с параллелизмом потоков, оно вызвано изменением резервного хранилища итератора. Для итератора не имеет значения, выполняется ли другой поток или нет. IMHO это плохо названное исключение, поскольку оно дает неверное представление о причине.
Робин
Однако я согласен с тем, что, если это непредсказуемо, скорее всего, существует проблема с потоками, которая вызывает условия для возникновения этого исключения. Что еще больше сбивает с толку из-за названия исключения.
Робин
Это правильное и лучшее объяснение, чем принятый ответ, но принятый ответ - хорошее исправление. ConcurrentHashMap не подлежит CME даже внутри итератора (хотя итератор по-прежнему предназначен для однопоточного доступа).
G__
В этом решении нет смысла, потому что у Maps нет метода iterator (). Пример Робина применим, например, к спискам.
Питер
72

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

Chochos
источник
Это действительно решило проблему? У меня такая же проблема, но я точно могу исключить любые проблемы с потоками.
tobiasbayer
5
Другое решение - создать копию карты и вместо этого перебирать эту копию. Или скопируйте набор ключей и перебирайте их, получая значение для каждого ключа из исходной карты.
Chochos
Именно Hibernate выполняет итерацию по коллекции, поэтому вы не можете просто скопировать ее.
tobiasbayer 01
1
Мгновенный спаситель. Пойду разберусь, почему это сработало так хорошо, чтобы в дальнейшем не преподнести больше сюрпризов.
Valchris
1
Я предполагаю, что это не проблема синхронизации, это проблема, если модификация одной и той же модификации при цикле одного и того же объекта.
Rais Alam
17

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

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

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

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

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

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

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

Документация из HashSet, HashMap, TreeSetи ArrayListклассов говорит , что это:

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

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

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

В документации к нескольким методам Mapинтерфейса сказано следующее:

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

Заметим еще раз, что для обнаружения требуется только «основа максимальных усилий», а ConcurrentModificationExceptionявно предлагается только для классов, которые не работают одновременно (небезопасные для потоков).

Отладка ConcurrentModificationException

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

  • Наиболее частой причиной является модификация Collectionвнутри расширенного forцикла над Collection. То, что вы не видите Iteratorобъекта в исходном коде, не означает, что его там нет Iterator! К счастью, один из операторов неисправного forцикла обычно находится в трассировке стека, поэтому отследить ошибку обычно легко.
  • Более сложный случай - это когда ваш код передает ссылки на Collectionобъект. Обратите внимание, что неизменяемые представления коллекций (например, созданные с помощью Collections.unmodifiableList()) сохраняют ссылку на изменяемую коллекцию, поэтому итерация по «неизменяемой» коллекции может вызвать исключение (модификация была сделана в другом месте). Другие взгляды из ваших Collection, таких как подсписки , Mapнаборы входных и Mapнаборы ключей также сохраняют ссылки на оригинал (изменяемый) Collection. Это может быть проблемой даже для потоковообеспеченных Collection, таких как CopyOnWriteList; не предполагайте, что потокобезопасные (параллельные) коллекции никогда не могут вызвать исключение.
  • Какие операции могут изменить a Collection, в некоторых случаях может быть неожиданным. Например, LinkedHashMap.get()изменяет свою коллекцию .
  • В самых сложных случаях исключение возникает из-за одновременной модификации несколькими потоками.

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

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

Raedwald
источник
Интересно, почему одновременная модификация не допускается в случае одного потока. Какие проблемы могут возникнуть, если одному потоку разрешено одновременное изменение обычной хэш-карты?
MasterJoe
4

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

map.keySet().removeIf(key -> key condition);
Zentopia
источник
2

Это звучит не столько как проблема синхронизации Java, сколько как проблема блокировки базы данных.

Я не знаю, поможет ли добавление версии ко всем вашим постоянным классам, но это один из способов, которым Hibernate может предоставить эксклюзивный доступ к строкам в таблице.

Может быть, уровень изоляции должен быть выше. Если вы разрешаете «грязное чтение», возможно, вам нужно увеличить до сериализуемого.

duffymo
источник
Я думаю, они имели в виду Hashtable. Он поставляется как часть JDK 1.0. Как и Vector, он был написан как потокобезопасный и медленный. Оба были заменены небезопасными альтернативами: HashMap и ArrayList. Платите за то, чем пользуетесь.
duffymo 08
0

Попробуйте либо CopyOnWriteArrayList, либо CopyOnWriteArraySet в зависимости от того, что вы пытаетесь сделать.

Javamann
источник
0

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

Я просто привожу здесь свой рабочий пример для новичков, чтобы сэкономить время:

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
источник
0

Я столкнулся с этим исключением при попытке удалить x последних элементов из списка. myList.subList(lastIndex, myList.size()).clear();было единственным решением, которое сработало для меня.

Grayman
источник