What does fail-fast vs fail-safe mean for iterators?
Quick Answer
Fail-fast iterators (ArrayList, HashMap, HashSet, ...) detect structural modification during iteration via a modCount check and throw ConcurrentModificationException as soon as they notice it — a best-effort safety net, not a guarantee. Fail-safe iterators (CopyOnWriteArrayList, ConcurrentHashMap) iterate over a snapshot or tolerate concurrent structural changes without throwing, though they may not reflect the very latest updates.
Detailed Answer
Fail-fast iterators (the default for ArrayList, HashMap, HashSet, LinkedList, etc.) track a modCount field that's incremented on every structural modification (add/remove, not just changing an existing element's value). Each iterator call (next(), etc.) checks the current modCount against the value captured when the iterator was created; a mismatch throws ConcurrentModificationException immediately:
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s); // throws ConcurrentModificationException
}
This is a best-effort detection mechanism meant to catch bugs early, not a hard guarantee — the JavaDoc explicitly warns it shouldn't be relied on for correctness (a modification could, in rare cases, go undetected).
Fail-safe (better termed "weakly consistent") iterators don't throw at all. CopyOnWriteArrayList iterates over an immutable snapshot of the array taken when the iterator was created — concurrent writes go to a new copy, so the iterator never sees them and never throws, but also never reflects the update. ConcurrentHashMap's iterator similarly tolerates concurrent modification, reflecting some but not necessarily all concurrent changes, again without throwing.
Practical takeaway: use iterator.remove()/listIterator methods to modify safely during iteration on fail-fast collections, or reach for a concurrent collection when multiple threads genuinely need to read and write at the same time.