Питання 26 · Розділ 4

Що таке fail-fast та fail-safe ітератори?

Важливо: fail-fast ловить зміни, зроблені будь-яким способом крім методів самого ітератора. Якщо ви викликаєте iterator.remove(), ітератор оновлює свій expectedModCount і CME не...

Мовні версії: English Russian Ukrainian

🟢 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-ітератори

  1. Часті записи — кожен запис копіює весь масив (CopyOnWriteArrayList)
  2. Великі списки — snapshot споживає O(n) пам’яті
  3. Потрібна актуальність даних — 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

  1. Fail-fast → індикатор багів логіки
  2. Snapshot → для listeners, рідкісних записів
  3. Weakly Consistent → high-concurrency
  4. modCount не volatile → best-effort
  5. removeIf > iterator.remove() для ArrayList
  6. 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]]