Explain Java's fail-fast iterators and ConcurrentModificationException. When is it thrown, and what guarantees does it actually provide?
answer
- modCount snapshot vs live → CME
- Only iterator.remove() keeps them in sync
- 'Concurrent' fires single-threaded too
- Best-effort: detect bugs, don't rely on it
- set() is not structural; concurrent collections are weakly consistent
basics
~10 sMost java.util collections return 'fail-fast' iterators. They count structural changes (modCount). If the collection is changed during iteration by anything other than the iterator's own remove(), the next next()/hasNext() throws ConcurrentModificationException.
solid answer
~50 sFail-fast iterators detect that the underlying collection was structurally modified mid-traversal and throw ConcurrentModificationException (CME) to surface the bug early instead of silently producing wrong results. The mechanism: the collection keeps a modCount, incremented on every structural change (add/remove that alters size). When you get an iterator it snapshots modCount as expectedModCount; each next()/remove() calls checkForComodification(), and if the two differ it throws. The iterator's own remove() updates expectedModCount, so it stays legal. Crucially, CME is best-effort, not a guarantee — the docs say you must not depend on it for correctness, only use it to catch bugs; under racing threads a change can go undetected. It also fires in single-threaded code (removing inside a for-each), so 'Concurrent' refers to concurrent structural modification, not necessarily multiple threads. Fully concurrent collections (CopyOnWriteArrayList, ConcurrentHashMap) instead give weakly consistent iterators that never throw CME.
code
java · 16 linesList<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4));
// CME: structural change not via the iterator
try {
for (Integer n : nums) {
if (n % 2 == 0) nums.remove(n); // bumps modCount
}
} catch (ConcurrentModificationException e) {
System.out.println("caught CME");
}
// No CME: replacing in place is NOT structural
ListIterator<Integer> li = nums.listIterator();
while (li.hasNext()) {
li.set(li.next() * 10); // legal, modCount unchanged
}go deeper
Knows you get an exception if you remove from a list while looping over it, and that iterator.remove() or removeIf avoids it.
Explains modCount vs expectedModCount and that only the iterator's own remove() stays in sync; knows set() is not structural.
Articulates that fail-fast is best-effort (bug detection, not a guarantee), that 'Concurrent' includes single-threaded cases, and contrasts weakly-consistent concurrent-collection iterators.
Reasons about iterator consistency models (fail-fast vs snapshot vs weakly-consistent) when choosing collections for concurrent systems and the correctness/throughput trade-offs each implies.
## What 'fail-fast' means A **fail-fast** system reports a detected error as soon as it occurs rather than letting the program continue in a corrupt state. For iterators, the failure being guarded against is: the collection you are walking gets **structurally modified** (an element added or removed, changing its size/shape) while a cursor is mid-walk. If unchecked, the cursor's cached position can go stale — you might skip elements, visit one twice, or read garbage. Java's general-purpose `java.util` collections choose to **fail fast**: they throw `ConcurrentModificationException` (CME) to make the bug loud and immediate. ## The modCount mechanism Every `AbstractList`/`HashMap`/etc. keeps an `int modCount` (modification count). It is incremented on each **structural modification** — an operation that adds or removes elements (e.g. `add`, `remove`, `clear`). Merely *replacing* an element via `set(i, x)` is **not** structural and does **not** bump `modCount`. When you create an iterator, it records the current value as `expectedModCount`: ```java int expectedModCount = modCount; ``` On each `next()` (and `remove()`), the iterator runs: ```java final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } ``` If the live `modCount` no longer matches the snapshot, something changed the collection behind the iterator's back, and it throws. The iterator's **own** `remove()` performs the deletion *and* resets `expectedModCount = modCount`, so removing through the iterator stays in sync and is legal — that is the one sanctioned way to delete during iteration. ## When is it thrown? - Removing/adding to a list inside a for-each loop over it (single-threaded — the classic case). - Two threads where one iterates and another structurally modifies. - Calling `map.put(newKey, v)` while iterating its `entrySet()` (note: re-putting an *existing* key is not structural and is allowed for many maps). Note the word **Concurrent** is misleading: it means *concurrent modification* (a modification interleaved with iteration), which happens in a single thread too. It does not require multiple threads. ## The guarantee it does NOT make The Javadoc is explicit: fail-fast behaviour is **best-effort and cannot be guaranteed**. Detection relies on an unsynchronized read of `modCount`, so under genuine multithreaded races the check may miss a modification (and conversely false-positives are conceivable). Therefore: > 'this exception should be used only to detect bugs.' You must **not** write logic that depends on a CME being thrown for correctness. It is a debugging aid, not a concurrency-control mechanism. Proper thread safety still requires external synchronization or a concurrent collection. ## Fail-fast vs weakly-consistent (fail-safe) iterators The concurrent collections take a different approach and never throw CME: - `CopyOnWriteArrayList`/`CopyOnWriteArraySet` iterate over a **snapshot** of the array taken when the iterator was created; later modifications are invisible to that iterator and it never throws. - `ConcurrentHashMap`'s iterators are **weakly consistent**: they traverse without locking, reflect *some* but not necessarily all concurrent updates, never throw CME, and never report an element twice. These are sometimes loosely called 'fail-safe', though 'weakly consistent' is the precise term. ## How to mutate safely during traversal 1. `Iterator.remove()` / `ListIterator.add()`/`set()` — the only in-loop structural ops that keep `modCount` in sync. 2. `Collection.removeIf(predicate)` — bulk, internally consistent. 3. Collect targets in a separate list and apply changes after the loop. 4. Switch to a concurrent collection when genuine concurrent access is the requirement. ## Summary Fail-fast iterators trade a hard, early `ConcurrentModificationException` for the chance of silently corrupt traversals. The detector is a `modCount` snapshot compared on each step; the iterator's own `remove()` is the sanctioned exception. Treat CME as a bug-finder, never as a guaranteed signal — and reach for concurrent collections when real concurrency is involved.
- Does ConcurrentModificationException always indicate multiple threads?No. It signals a modification interleaved with iteration, which routinely happens in single-threaded code (e.g. removing inside a for-each). The 'Concurrent' refers to concurrent-with-iteration, not necessarily multi-threaded.
- How do CopyOnWriteArrayList iterators avoid CME?Their iterator captures a reference to the backing array snapshot at creation time. Subsequent mutations create a new array, so the iterator keeps reading the old snapshot and never sees a modification to throw on.
saying these in an interview costs you the question
- Saying CME is a reliable thread-safety guarantee — it is best-effort only
- Claiming CME requires multiple threads
- Thinking set()/replace counts as a structural modification
- Believing the iterator's own remove() triggers CME (it is the sanctioned safe path)