Модификация 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
бросает ConcurrentModificationException
. Таким образом, перед одновременной модификацией итератор быстро и чисто дает сбой, вместо того, чтобы рисковать произвольным недетерминированным поведением в неопределенное время в будущем.
Обратите внимание, что отказоустойчивое поведение итератора не может быть гарантировано, поскольку вообще невозможно сделать какие-либо жесткие гарантии при наличии несинхронизированной одновременной модификации. Отказоустойчивые итераторы отбрасывают ConcurrentModificationException
с максимальной отдачей. Следовательно, было бы неправильно писать программу, которая зависела от этого исключения в отношении его корректности: Поведение итераторов при быстром отказе следует использовать только для обнаружения ошибок .
Обратите внимание, что поведение «не может быть гарантировано» и действует только «на основе максимальных усилий».
Документация нескольких методов интерфейса Map
гласит:
Неконкурентные реализации должны переопределить этот метод и, с максимальной отдачей, выбросить ConcurrentModificationException
, если обнаружено, что функция отображения изменяет эту карту во время вычисления. Параллельные реализации должны переопределить этот метод и, с максимальной отдачей, выбросить 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
a private
или локальную переменную и не возвращайте ссылки на Collection
или его итераторы из методов. Тогда намного проще исследовать все места, где Collection
можно изменить. Если Collection
будет использоваться несколькими потоками, тогда практично обеспечить, чтобы потоки обращались к Collection
только с соответствующей синхронизацией и блокировкой.