Почему возникает исключение 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)

person mainstringargs    schedule 02.03.2009    source источник
comment
Не могли бы вы предоставить дополнительный контекст? Вы объединяете, обновляете или удаляете объект? Какие ассоциации есть у этого объекта? А как насчет ваших настроек каскадирования?   -  person ordnungswidrig    schedule 03.03.2009
comment
Из трассировки стека вы можете видеть, что исключение происходит во время итерации HashMap. Конечно, какой-то другой поток изменяет карту, но исключение возникает в потоке, который выполняет итерацию.   -  person Chochos    schedule 31.08.2010


Ответы (8)


Это не проблема синхронизации. Это произойдет, если базовая коллекция, по которой выполняется итерация, будет изменена чем-либо, кроме самого 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().

person Robin    schedule 02.03.2009
comment
Возможно, но похоже, что Hibernate выполняет итерацию, которая должна быть реализована разумно правильно. Возможен обратный вызов, изменяющий карту, но это маловероятно. Непредсказуемость указывает на реальную проблему параллелизма. - person Tom Hawtin - tackline; 02.03.2009
comment
Это исключение не имеет ничего общего с параллелизмом потоков, оно вызвано изменением резервного хранилища итератора. Для итератора не имеет значения, выполняется ли другой поток или нет. ИМХО, это плохо названное исключение, поскольку оно дает неверное представление о причине. - person Robin; 02.03.2009
comment
Однако я согласен с тем, что, если это непредсказуемо, скорее всего, существует проблема с потоками, которая вызывает условия для возникновения этого исключения. Что еще больше сбивает с толку из-за названия исключения. - person Robin; 02.03.2009
comment
Это правильное и лучшее объяснение, чем принятый ответ, но принятый ответ - хорошее исправление. ConcurrentHashMap не подлежит CME даже внутри итератора (хотя итератор по-прежнему предназначен для однопоточного доступа). - person G__; 10.05.2011
comment
В этом решении нет смысла, потому что у Maps нет метода iterator (). Пример Робина применим, например, к Списки. - person peter; 21.08.2012
comment
Я думаю, что точка все еще была обслужена, но вы правы в том, что мой пример был неправильным. Это было исправлено. - person Robin; 25.10.2012
comment
Я работал над Android, и мне потребовалось около 4 часов, чтобы найти что-то, что указывало бы на мою проблему. Большое спасибо. - person Muneeb Mirza; 24.08.2016

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

person Chochos    schedule 02.03.2009
comment
Это действительно решило проблему? У меня такая же проблема, но я точно могу исключить какие-либо проблемы с потоками. - person tobiasbayer; 31.08.2010
comment
Другое решение - создать копию карты и вместо этого перебирать эту копию. Или скопируйте набор ключей и перебирайте их, получая значение для каждого ключа из исходной карты. - person Chochos; 31.08.2010
comment
Это Hibernate, который выполняет итерацию по коллекции, поэтому вы не можете просто скопировать ее. - person tobiasbayer; 01.09.2010
comment
Мгновенный спаситель. Пойду разберусь, почему это сработало так хорошо, чтобы больше не преподносить сюрпризов в будущем. - person Valchris; 16.03.2011
comment
@CodeBrickie, да, это как-то решает проблему, о которой писал @Robin. В любом случае map = Collections.synchronizedMap(map); ничего не решает :) Так что это не проблема с потоками, похоже, что ConcurrentHashMap более безопасен для нубов :) - person dantuch; 10.06.2012
comment
ConcurrentHashMap можно изменить во время итерации, и он не будет генерироваться. Обратной стороной является более низкая производительность. - person Chochos; 14.06.2012
comment
Я предполагаю, что это не проблема синхронизации, это проблема, если модификация одной и той же модификации при цикле одного и того же объекта. - person Rais Alam; 24.12.2012
comment
Из документации ConcurrentHashMap Similarly, Iterators, Spliterators and Enumerations return elements reflecting the state of the hash table at some point at or since the creation of the iterator/enumeration. They do not throw ConcurrentModificationException. However, iterators are designed to be used by only one thread at a time. - person juanmf; 15.12.2016

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

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

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

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

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

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

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

Документация HashSet, _ 19_, _ 20_ и _ 21_ классы говорят следующее:

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

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

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

Документация по нескольким методам интерфейса Map. скажи это:

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

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

Отладка ConcurrentModificationException

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

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

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

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

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

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

map.keySet().removeIf(key -> key condition);
person Zentopia    schedule 20.11.2019
comment
Красивый. Это должно быть наверху. Это время O (n), время O (1)? - person PlsWork; 13.01.2021
comment
Это более элегантное решение. Спасибо. - person S_K; 16.06.2021

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

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

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

person duffymo    schedule 02.03.2009
comment
Я думаю, они имели в виду Hashtable. Он поставляется как часть JDK 1.0. Как и Vector, он был написан, чтобы быть потокобезопасным - и медленным. Оба они были заменены альтернативами, не обеспечивающими потокобезопасность: HashMap и ArrayList. Платите за то, чем пользуетесь. - person duffymo; 09.10.2014

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

person Javamann    schedule 02.03.2009

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

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

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();
    }
person ZhaoGang    schedule 06.07.2018

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

person grayman    schedule 15.06.2020