Модификация в 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только с соответствующей синхронизацией и блокировкой.