All topics
library
intermediate

Fail-Fast vs Fail-Safe Iterators

Understand why ArrayList throws ConcurrentModificationException but ConcurrentHashMap doesn't.

Two different answers to "the collection changed while I was iterating".

Fail-fast = a security guard who immediately stops you if someone else enters the room during your presentation. Fail-safe = a camera recording — keeps going regardless of who enters, but the recording might miss late arrivals.

Key Concepts

1
    fail-fast   ArrayList, HashMap, HashSet
                throws ConcurrentModificationException
    fail-safe   ConcurrentHashMap, CopyOnWriteArrayList
                never throws; may not see recent changes
2
How fail-fast works. The collection keeps a modCount, bumped on every structural change. The iterator records it at creation and checks it on every next():
modCountnext()
3
    final void checkForComodification() {
      if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    }
4
Which is why this fails on the second iteration, not the removal:
    for (String s : list)
      if (s.isEmpty()) list.remove(s);
5
The remove succeeds. The next next() notices the count moved and throws — so the stack trace points at the loop, not at the line that caused it.
removenext()
6
And why removing the second-to-last element appears to work. After the removal, hasNext() compares the cursor against the new smaller size, returns false, and the loop exits before any check runs. The same code then throws for any other element, which makes it look intermittent.
hasNext()
7
CopyOnWriteArrayList iterates a snapshot.
    var list = new CopyOnWriteArrayList<>(List.of("a", "b"));
    for (String s : list) list.add("c");       // terminates, no exception
CopyOnWriteArrayList
8
The iterator holds the array as it was when created, so writes it cannot see. No exception, and no view of the additions either.
9
ConcurrentHashMap is weakly consistent. Its iterator reflects the map at some point during traversal: every entry present at the start appears, and entries added during it may or may not. It never throws.
ConcurrentHashMap
10
Fail-fast is a debugging aid, not a guarantee. The modCount check has no synchronisation, so a change from another thread may go unseen. Do not catch the exception; use a concurrent collection or hold a lock.
modCount
11
The fixes, in order of preference.
    list.removeIf(pred)              clearest
    iterator.remove()                when you need more control
    collect, then modify after the loop
    a concurrent collection          when threads really are sharing it
12
What the interviewer is probing.1. "What makes an iterator fail-fast?" Probing: the mechanism, not just the label. Stalls: "It checks for changes." Moves up: the collection keeps a modCount, the iterator records it and compares on every next(), throwing ConcurrentModificationException when it differs.
13
2. "Can you rely on catching ConcurrentModificationException?" Probing: whether you know it is best effort. Stalls: "Yes, catch and retry." Moves up: no — the check has no synchronisation, so a change from another thread may go unseen; it is a debugging aid, not a control mechanism.
14
3. "How does CopyOnWriteArrayList avoid it?" Probing: the snapshot. Stalls: "It locks." Moves up: its iterator holds the array as it was at creation, so writes during the loop are simply invisible to it — and iterator.remove is unsupported.
15
4. "What does 'weakly consistent' mean for ConcurrentHashMap?" Probing: precision about the guarantee. Stalls: "It might be wrong." Moves up: every entry present at the start is seen, entries added during traversal may or may not be, and it never throws.