Що таке fail-fast та fail-safe ітератори?
Важливо: fail-fast ловить зміни, зроблені будь-яким способом крім методів самого ітератора. Якщо ви викликаєте iterator.remove(), ітератор оновлює свій expectedModCount і CME не...
🟢 Junior Level
Fail-fast — ітератор одразу кидає виняток, якщо колекцію змінили. Fail-safe — ітератор продовжує працювати, ігноруючи зміни.
// Fail-fast (ArrayList, HashMap)
List<String> list = new ArrayList<>(List.of("A", "B"));
for (String s : list) {
list.add("C"); // → ConcurrentModificationException!
}
// Fail-safe (CopyOnWriteArrayList, ConcurrentHashMap)
List<String> safe = new CopyOnWriteArrayList<>(List.of("A", "B"));
for (String s : safe) {
safe.add("C"); // → OK (ітератор не бачить "C")
}
🟡 Middle Level
Fail-fast: modCount механізм
// Колекція зберігає modCount (число змін)
// Ітератор копіює expectedModCount при створенні
// При кожному next():
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
Важливо: fail-fast ловить зміни, зроблені будь-яким способом крім методів самого ітератора. Якщо ви викликаєте iterator.remove(), ітератор оновлює свій expectedModCount і CME не виникне. Це пастка для відлову багів в одному потоці, а не механізм синхронізації.
Типи “fail-safe”
Snapshot-based:
// CopyOnWriteArrayList
Iterator<String> it = list.iterator(); // Snapshot масиву
list.add("X"); // Новий масив
it.next(); // → З старого масиву (не бачить "X")
Weakly Consistent:
// ConcurrentHashMap
Iterator<String> it = map.keySet().iterator();
map.put("X", 1); // Живі дані
it.next(); // → МОЖЕ побачити "X" (але не зобов'язаний!)
// → Як правило, не кидає CME (для ConcurrentHashMap — гарантовано ні)
Порівняння
| Fail-fast | Snapshot | Weakly Consistent | |
|---|---|---|---|
| Exception | Так | Ні | Ні |
| Копія | Ні | Так | Ні |
| Актуальність | Строга | Застаріла | Приблизна |
🔴 Senior Level
Коли НЕ використовувати snapshot-ітератори
- Часті записи — кожен запис копіює весь масив (CopyOnWriteArrayList)
- Великі списки — snapshot споживає O(n) пам’яті
- Потрібна актуальність даних — snapshot застаріває миттєво після створення
modCount не volatile!
// У багатопотоковому середовищі:
// Потік 1 змінює → modCount++
// Потік 2 може НЕ побачити (cache coherence)!
// → CME не гарантований у multi-threading!
// Це best-effort, не 100% захист
volatile — ключове слово, що гарантує видимість змін між потоками. Без volatile один потік може записати modCount++, а інший потік (зі свого CPU-кешу) побачити старе значення.
Best-effort — механізм намагається спіймати помилку, але не гарантує в multi-threading. Fail-fast — це детектор багів, а не механізм синхронізації.
removeIf оптимізація
// ❌ O(n²) для ArrayList
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
if (condition(it.next())) {
it.remove(); // System.arraycopy щоразу!
}
}
// ✅ O(n) один прохід
list.removeIf(e -> condition(e));
// → Спочатку знаходить всі для видалення
// → Потім ОДИН зсув масиву
Snapshot memory cost
CopyOnWriteArrayList: 1 млн елементів
→ Iterator = посилання на масив (4 МБ)
→ add() = новий масив (4 МБ)
→ Тимчасово: 8 МБ!
Best Practices
- Fail-fast → індикатор багів логіки
- Snapshot → для listeners, рідкісних записів
- Weakly Consistent → high-concurrency
- modCount не volatile → best-effort
- removeIf > iterator.remove() для ArrayList
- iterator.remove() → єдиний безпечний спосіб у fail-fast
Резюме для Senior
- Fail-fast = modCount перевірка, best-effort
- Snapshot = копія масиву, ізоляція
- Weakly Consistent = живі дані, без Exception
- modCount не volatile → не 100% у multi-threading
- removeIf = O(n) оптимізація
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- Fail-fast ітератори кидають
ConcurrentModificationExceptionпри зміні колекції (ArrayList, HashMap) - Механізм: порівняння
modCountколекції зexpectedModCountітератора - Fail-fast — це детектор багів в одному потоці, а не механізм синхронізації
- Snapshot-ітератори (CopyOnWriteArrayList) — копія масиву на момент створення, не бачать подальші зміни
- Weakly Consistent (ConcurrentHashMap) — живі дані, без CME, але не гарантують актуальність
modCountНЕ volatile → у багатопотоковості CME не гарантований (best-effort)iterator.remove()— єдиний безпечний спосіб видалення при fail-fast ітераціїremoveIf()кращеiterator.remove()для ArrayList — O(n) замість O(n^2)
Часті уточнюючі запитання:
- Чому fail-fast не гарантує CME у багатопотоковості? —
modCountне volatile; потік може не побачити зміну через cache coherence. - Що бачить ітератор CopyOnWriteArrayList після add()? — Старі дані (snapshot); нові елементи не видні.
- Чим Snapshot відрізняється від Weakly Consistent? — Snapshot — копія (застарілі дані), Weakly Consistent — живі дані (можуть бачити зміни).
- Яка ціна snapshot для 1 млн елементів? — O(n) пам’яті; копіювання масиву ~4 МБ, при add() — тимчасово 8 МБ.
Червоні прапорці (НЕ говорити):
- «Fail-fast гарантує виявлення змін у багатопотоковості» — ні, modCount не volatile, best-effort
- «Snapshot-ітератор бачить нові елементи» — ні, snapshot — копія на момент створення
- «CopyOnWriteArrayList підходить для частих записів» — ні, кожен запис = копія всього масиву
- «Fail-fast — це механізм синхронізації» — ні, це детектор багів логіки
Пов’язані теми:
- [[27. Що таке ConcurrentModificationException]]
- [[28. Як правильно видаляти елементи під час ітерації]]
- [[21. Коли використовувати synchronized collections]]