Что такое ConcurrentModificationException?
subList не создаёт новый список — это view на оригинал. SubList хранит expectedModCount родителя. Когда вы меняете родительский список напрямую, его modCount меняется, а expecte...
🟢 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
- CME = нарушение контракта итератора
- Best-effort → не гарантия в multi-threading
- Views (subList, keySet) → чувствительны к modCount
- removeIf → оптимальный способ
- iterator.remove() → безопасный в цикле
- 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]]