Автор заметки — Гжегож Мирек — разработчик программного обеспечения из Кракова (Польша). Он занялся разработкой на Java около 6 лет назад, ещё в университете, и, с этого времени, неустанно шлифует своё мастерство в данной сфере. Его особенно интересует вопрос производительности JVM и оптимизации, о чем он, в основном, и пишет в своём блоге.
Среди наиболее популярных вопросов на собеседованиях по языку Java есть и такой:
В чём различие между fail-fast и fail-safe итераторами?
Максимально упрощённый ответ на него:
Fail-fast итератор генерирует исключение ConcurrentModificationException, если коллекция меняется во время итерации, а fail-safe – нет.
Хотя это звучит достаточно осмысленно, остается непонятным, что интервьюер понимает под fail-safe? Спецификации языка Java не определяют этот термин в отношении итераторов. Однако существуют четыре стратегии конкурентной модификации.
Конкурентная модификация
Во-первых, давайте определимся, что такое конкурентная (или параллельная) модификация. Допустим у нас есть коллекция и при активном итераторе происходят какие-либо её изменения, не исходящие от данного итератора. В таком случае у нас получается конкурентная модификация. Приведу простейший пример: допустим, у нас есть несколько нитей. Первая нить выполняет итерации, а вторая вставляет элементы в ту же коллекцию или удаляет их из неё. Однако мы можем получить исключение ConcurrentModificationException и при работе в однопоточной среде:List<String> cities = new ArrayList<>();
cities.add(“Warsaw”);
cities.add(“Prague”);
cities.add(“Budapest”);
Iterator<String> cityIterator = cities.iterator();
cityIterator.next();
cities.remove(1);
cityIterator.next(); // генерирует ConcurrentModificationException
Fail-fast
Вышеприведенный фрагмент кода – пример fail-fast итератора. Как вы можете видеть, при попытке извлечения второго элемента из итератора было сгенерировано исключение ConcurrentModificationException. Откуда итератор узнает, что коллекция была модифицирована после его создания? Например, в коллекции может быть метка даты/времени, скажем, lastModified. При создании итератора вам стоит скопировать это поле и сохранить его в объекте итератора. Затем, при каждом вызове метода next(), нужно будет просто сравнить значение lastModified из коллекции с копией из итератора. Очень близкий подход используется, например, в реализации класса ArrayList. В нём есть переменная экземпляра modCount, в которой хранится количество модификаций списка:final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
Важно отметить, что fail-fast итераторы работают на основе принципа "по мере возможности", то есть не дается никаких гарантий генерации исключения ConcurrentModificationException в случае конкурентной модификации. Так что полагаться на это не стоит – скорее, их следует использовать для обнаружения ошибок. Большинство неконкурентных коллекций предоставляют fail-fast итераторы.
Слабая согласованность
Большинство конкурентных коллекций из пакета java.util.concurrent (например, ConcurrentHashMap и большинство Queue) предоставляют слабо согласованные итераторы. Смысл этого термина очень хорошо разъясняется в документации:- Они могут обрабатываться конкурентно с другими операциями
- Они никогда не генерируют исключение ConcurrentModificationException
- Они гарантированно обходят существовавшие на момент создания итератора элементы ровно один раз, и могут (но не обязаны) отражать последующие модификации.