Что такое fail-fast и fail-safe итераторы
При обходе коллекций в Java поведение итератора в случае модификации коллекции делится на две основные стратегии:
🟢 Junior Level
1. Простое определение концепций
При обходе коллекций в Java поведение итератора в случае модификации коллекции делится на две основные стратегии:
- Fail-Fast («Быстрый отказ»): итератор немедленно выбрасывает исключение
ConcurrentModificationException, если обнаруживает, что коллекция была изменена (добавлен, удален или заменен элемент) в обход методов самого итератора. - Fail-Safe («Безопасный отказ»): итератор не падает с исключением при параллельной модификации коллекции, продолжая корректный обход элементов.
// 1. Fail-Fast (ArrayList, HashMap, HashSet)
List<String> fastList = new ArrayList<>(List.of("A", "B"));
for (String s : fastList) {
fastList.add("C"); // КАТАСТРОФА: выбросит ConcurrentModificationException!
}
// 2. Fail-Safe (CopyOnWriteArrayList, ConcurrentHashMap)
List<String> safeList = new CopyOnWriteArrayList<>(List.of("A", "B"));
for (String s : safeList) {
safeList.add("C"); // БЕЗОПАСНО: итератор спокойно завершит цикл (обошел "A", "B")
}
2. Сравнительная таблица
| Критерий | Fail-Fast |
Fail-Safe (Snapshot) |
Weakly Consistent |
|---|---|---|---|
Выброс CME |
✅ Да (при любой сторонней модификации) | ❌ Никогда | ❌ Никогда |
| Примеры коллекций | ArrayList, HashSet, HashMap |
CopyOnWriteArrayList, CopyOnWriteArraySet |
ConcurrentHashMap, ConcurrentLinkedQueue |
| Работа с копией | Нет (прямой доступ к данным) | Да (неизменяемый снимок массива) | Нет (живые данные через volatile) |
| Видит ли новые данные | Падает до того, как успеет увидеть | ❌ Нет (изолирован в снимке) | ⚠️ Может увидеть, но не гарантирует |
| Накладные расходы | Минимальные ($O(1)$) | Высокие при записи ($O(N)$ копирование) | Минимальные (Lock-Free) |
🟡 Middle Level
1. Механизм работы Fail-Fast: переменные modCount и expectedModCount
Внутри коллекций с fail-fast поведением определен счетчик структурных изменений:
protected transient int modCount = 0;
Любое действие, меняющее размер или внутреннее состояние структуры данных (add, remove, clear, resize), увеличивает modCount++.
Когда создается итератор, он запоминает текущее значение счетчика:
int expectedModCount = modCount;
При каждом шаге обхода в методах next() и remove() проверяется инвариант:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
Если коллекция была модифицирована напрямую (например, list.add("X")), modCount увеличился, а expectedModCount итератора остался старым. На следующем же вызове it.next() происходит немедленный выброс исключения.
[!NOTE] Если элемент удаляется через метод самого итератора (
it.remove()), итератор синхронизирует счетчики:expectedModCount = modCount, поэтому исключение не возникает.
2. Терминологический нюанс: официальная позиция спецификации JDK
[!IMPORTANT] Термин «Fail-Safe» не является официальным термином спецификации Java (JLS / Javadoc)! Это устоявшийся сленг разработчиков.
Официальная документация JDK классифицирует неколлидирующие итераторы на две строгие категории:
- Snapshot-style Iterators (Снимочные): работают по фиксированной неизменяемой копии данных (
CopyOnWriteArrayList).- Weakly Consistent Iterators (Слабосогласованные): работают с живой структурой данных, гарантируя отсутствие
CME, прохождение по всем элементам на момент создания и отсутствие дубликатов (ConcurrentHashMap).
🔴 Senior Level
1. Почему Fail-Fast — это «Best-Effort», а не гарантия многопоточности?
Javadoc класса ArrayList содержит важное предупреждение:
«Обратите внимание, что поведение fail-fast не может быть гарантировано, так как невозможно предоставить жесткие гарантии в присутствии несинхронизированных параллельных модификаций… Fail-fast итераторы должны использоваться только для обнаружения ошибок программы».
Аппаратная причина:
Переменная modCount объявлена как обычный примитив int, она НЕ является volatile:
protected transient int modCount = 0; // Нет ключевого слова volatile!
Если Поток 1 изменяет список в ядре CPU 1, значение modCount++ оседает в локальном кэше L1/L2 процессора и не сбрасывается в общую память немедленно. Поток 2, выполняющий итерацию на ядре CPU 2, может прочитать устаревшее значение modCount из своего кэша, пропустить проверку modCount != expectedModCount и продолжить итерацию по разрушенному массиву, приводя к повреждению данных или непредсказуемому ArrayIndexOutOfBoundsException.
Вывод: Fail-fast — это детектор логических ошибок в однопоточном коде, а не механизм синхронизации для многопоточности.
2. Сравнение производительности: iterator.remove() против removeIf()
При удалении элементов из ArrayList во время обхода часто используют iterator.remove(). Однако для списков на массивах это порождает тяжелую проблему производительности:
// ❌ СЛОЖНОСТЬ O(N^2):
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().startsWith("bad_")) {
it.remove(); // Каждый вызов делает System.arraycopy для сдвига хвоста!
}
}
// ✅ СЛОЖНОСТЬ O(N) в Java 8+:
list.removeIf(s -> s.startsWith("bad_"));
Метод removeIf в ArrayList сначала сканирует массив с помощью битовой маски BitSet, помечая индексы удаляемых элементов за один проход, а затем выполняет ровно одно компактирующее перемещение данных.
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«
Fail-Fastитераторы (вArrayList,HashMap) обнаруживают структурные изменения коллекции, сделанные не через методы самого итератора, и немедленно выбрасываютConcurrentModificationException. Это реализуется сравнением счетчикаmodCountколлекции сexpectedModCountитератора. ТерминFail-Safeнеофициальный: под ним понимают либо снимочные итераторы (CopyOnWriteArrayList), которые обходят неизменяемую копию массива, либо слабосогласованные итераторы (ConcurrentHashMap), которые обходят живыеvolatile-узлы без блокировок и без рискаCME. Механизм fail-fast не гарантирует защиту в многопоточности, так какmodCountне являетсяvolatile».
3 каверзных вопроса на интервью с ответами
1. Может ли однопоточная программа без параллельных потоков выбросить ConcurrentModificationException?
Ответ: Да, и это самая распространенная причина этого исключения на практике. Если внутри обычного цикла for-each вызвать прямой метод модификации коллекции (например, list.remove(item) или list.add(item)), коллекция инкрементирует modCount. На следующем шаге цикла итератор обнаружит несовпадение modCount и expectedModCount и выбросит ConcurrentModificationException.
2. Почему разработчики JDK не сделали поле modCount переменной volatile?
Ответ: Ключевое слово volatile налагает заметный оверхед на производительность, требуя сброса кэша и генерации барьеров памяти (StoreLoad / Memory Barrier) на каждую операцию добавления или удаления элемента. Коллекции пакета java.util (как ArrayList) проектировались как максимально легковесные и быстрые однопоточные структуры. Делать modCount volatile ради негарантированной диагностики в изначально некорректном многопоточном коде было сочтено неоправданным компромиссом по скорости.
3. Что произойдет, если в процессе итерации по ConcurrentHashMap другой поток удалит текущий элемент?
Ответ: Никакого исключения выброшено не будет. Итератор ConcurrentHashMap является слабосогласованным (Weakly Consistent). Он удерживает ссылку на текущий Node, поля которого объявлены как volatile. Если элемент удаляется из мапы, его ссылки в хэш-таблице перестраиваются, но сам объект ноды продолжает корректно ссылаться на следующий узел цепочки через node.next, и итератор успешно продолжит обход оставшихся элементов.
Типичные ошибки и Red Flags
- ❌ Утверждение, что
ConcurrentModificationExceptionвозникает только в многопоточных программах: Чаще всего возникает в одном потоке при вызовеlist.remove()внутри циклаfor-each. - ❌ Вера в то, что fail-fast надежно защищает многопоточный код:
modCountне volatile, проверка носит статус Best-Effort. - ❌ Попытка использовать
iterator.remove()вCopyOnWriteArrayList: ВызоветUnsupportedOperationException, так как снимок неизменяем. - ❌ Ожидание, что снимочный итератор увидит элементы, добавленные параллельно: Снимок полностью изолирован от последующих записей.