Модификация в 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
объектом, чтобы было легче предотвратить одновременные изменения. Сделайте объект Collection
a private
или локальную переменную и не возвращайте ссылки на Collection
итераторы из методов. Тогда гораздо легче обследовать все места, гдеCollection
можно изменить. Если Collection
должен использоваться несколькими потоками, тогда целесообразно гарантировать, что потоки обращаются к нему Collection
только с соответствующей синхронизацией и блокировкой.