Вопрос 27 · Раздел 4

Что такое ConcurrentModificationException?

subList не создаёт новый список — это view на оригинал. SubList хранит expectedModCount родителя. Когда вы меняете родительский список напрямую, его modCount меняется, а expecte...

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

CME (ConcurrentModificationException) — сигнал о том, что код нарушил контракт итератора: коллекция была изменена НЕ через методы самого итератора.

Важно: CME — это НЕ ошибка многопоточности (как можно подумать по названию). Это ловушка для отлова багов в одном потоке.

List<String> list = new ArrayList<>(List.of("A", "B", "C"));

// ❌ Ошибка!
for (String s : list) {
    list.remove(s);  // → ConcurrentModificationException!
}

// ✅ Правильно:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    it.remove();  // OK!
}

Важно: Чаще бывает в одном потоке, а не в многопоточности!


🟡 Middle Level

Механика: modCount

// Коллекция: modCount (число изменений)
// Итератор: expectedModCount (копия при создании)

// При next():
if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
}

Коварные сценарии

SubList ловушка:

List<String> sub = list.subList(0, 2);
list.add("X");       // Изменили родителя
sub.size();          // → CME! SubList чувствителен к modCount родителя

subList не создаёт новый список — это view на оригинал. SubList хранит expectedModCount родителя. Когда вы меняете родительский список напрямую, его modCount меняется, а expectedModCount в SubList — нет. Проверка обнаруживает рассинхронизацию.

Последний элемент:

// Иногда CME НЕ вылетает при удалении предпоследнего!
// → hasNext() возвращает false → цикл завершается
// → Логическая ошибка не поймана!

Если удалить предпоследний элемент через list.remove(), hasNext() может уже вернуть false (итератор «думает», что элементов нет). Цикл завершается без проверки modCount → CME не вылетает, но данные уже повреждены. Это не баг CME — это best-effort детектор.

Способы решения

// 1. Iterator.remove()
it.remove();  // Обновляет expectedModCount

// 2. removeIf (Java 8+)
list.removeIf(e -> condition(e));

// 3. Collect to remove
List<String> toRemove = new ArrayList<>();
for (String s : list) {
    if (condition(s)) toRemove.add(s);
}
list.removeAll(toRemove);

// 4. Concurrent коллекции
CopyOnWriteArrayList<String> safe = new CopyOnWriteArrayList<>(list);
// → Итератор = snapshot, не бросает CME

🔴 Senior Level

CME ≠ многопоточность

CME чаще в ОДНОМ потоке!
  → Удаление в for-each цикле
  
В многопоточности без синхронизации:
  → modCount не volatile
  → Поток может НЕ увидеть изменение
  → ArrayIndexOutOfBoundsException вместо CME!

Views чувствительны к modCount

// keySet, entrySet, subList → view на оригинал
// Изменение оригинала → CME в view!

Map<String, Integer> map = new HashMap<>();
Set<String> keys = map.keySet();
map.put("A", 1);      // Изменили map
for (String k : keys) { ... }  // → CME!

View (представление) — объект, который не хранит свои данные, а показывает данные оригинальной коллекции. Изменения в оригинале мгновенно видны через view, и наоборот.

Best Practices

  1. CME = нарушение контракта итератора
  2. Best-effort → не гарантия в multi-threading
  3. Views (subList, keySet) → чувствительны к modCount
  4. removeIf → оптимальный способ
  5. iterator.remove() → безопасный в цикле
  6. Concurrent коллекции → weakly consistent, без CME

Резюме для Senior

  • modCount = детектор изменений
  • Best-effort → не 100% в multi-threading
  • Views → CME при изменении оригинала
  • removeIf > iterator.remove() > collect-and-remove
  • Concurrent коллекции → нет CME

🎯 Шпаргалка для интервью

Обязательно знать:

  • CME возникает при изменении коллекции НЕ через методы итератора (чаще в одном потоке!)
  • Механизм: modCount коллекции != expectedModCount итератора при вызове next()
  • CME — не ошибка многопоточности, а ловушка для отлова багов логики
  • subList, keySet, entrySet — view-объекты, чувствительны к modCount родителя
  • При удалении предпоследнего элемента CME может НЕ вылететь (best-effort детектор)
  • 4 способа решения: iterator.remove(), removeIf(), collect-and-remove, concurrent коллекции
  • iterator.remove() обновляет expectedModCount — CME не возникает

Частые уточняющие вопросы:

  • Почему CME чаще в одном потоке, а не в многопоточности? — Самый частый сценарий: удаление элемента в for-each цикле одним потоком.
  • Почему subList бросает CME при изменении родителя? — SubList хранит expectedModCount родителя; прямое изменение родителя рассинхронизирует.
  • Что будет в многопоточности без синхронизации вместо CME?ArrayIndexOutOfBoundsException — поток не видит изменение modCount из-за отсутствия volatile.
  • Почему CME может не вылететь при удалении предпоследнего элемента?hasNext() может вернуть false до проверки modCount — цикл завершается без детекции.

Красные флаги (НЕ говорить):

  • «CME — это ошибка многопоточности» — нет, чаще всего возникает в одном потоке
  • «CME гарантирует обнаружение изменений в многопоточности» — нет, best-effort, modCount не volatile
  • «subList создаёт независимый список» — нет, это view на оригинал, чувствительный к modCount
  • «Всегда можно положиться на CME для синхронизации» — это детектор багов, не механизм синхронизации

Связанные темы:

  • [[26. Что такое fail-fast и fail-safe итераторы]]
  • [[28. Как правильно удалять элементы во время итерации]]
  • [[25. В чём разница между Iterator и ListIterator]]