Fail-fast vs fail-safe iterators
MediumFail-fast iterators (ArrayList, HashMap) track a modification counter and throw ConcurrentModificationException if the collection changes behind their back. "Fail-safe" ones (CopyOnWriteArrayList, ConcurrentHashMap) walk a snapshot or a live view and never throw.
How it works
- modCount. Non-concurrent collections keep an
int modCountthat goes up on every structural change (add, remove, resize, clear). Setting an existing element's value doesn't count. - Fail-fast check. An iterator copies
modCountintoexpectedModCountwhen created. Eachnext()compares the two; a mismatch throwsConcurrentModificationException. - The iterator's own
remove()changes the collection and updatesexpectedModCount, so it's the safe way to delete while looping. - Snapshot iterators.
CopyOnWriteArrayListcopies its array on every write. An iterator holds the array that existed when it was created, so it never sees later changes and never throws. - Weakly consistent iterators.
ConcurrentHashMapandConcurrentLinkedQueueiterate the live structure. They may or may not show changes made during iteration, and they never throw CME.
The JDK docs don't use "fail-safe"; they say snapshot or weakly consistent. Interviewers still use the term.
Example
List<String> tasks = new ArrayList<>(List.of("draft", "spam", "review", "spam"));
// Throws ConcurrentModificationException
for (String t : tasks) {
if (t.equals("spam")) tasks.remove(t);
}
// Fine: the iterator removes and stays in sync
for (Iterator<String> it = tasks.iterator(); it.hasNext(); ) {
if (it.next().equals("spam")) it.remove();
}
// Simplest (Java 8+)
tasks.removeIf(t -> t.equals("spam"));
// Snapshot: loop sees the original 2 items; list ends as [a, b, a!, b!]
List<String> cow = new CopyOnWriteArrayList<>(List.of("a", "b"));
for (String s : cow) cow.add(s + "!"); // no exception, no infinite loopEdge cases
- Removing the second-to-last element inside a for-each over an
ArrayListdoesn't throw:hasNext()seescursor == sizeand the loop just ends, silently skipping the last element. - Fail-fast is best effort. Two threads racing on an
ArrayListmay corrupt it without any exception being thrown. CopyOnWriteArrayList's iterator doesn't supportremove(); it throwsUnsupportedOperationException.
Common mistakes
- Thinking CME is only about threads. Most real cases are a single thread modifying the list it's looping over.
- Catching CME to "handle" concurrency. Use a concurrent collection or proper locking.
- Using
CopyOnWriteArrayListfor write-heavy lists. Every add copies the whole array.
Likely follow-up
"Does ConcurrentHashMap guarantee you'll see an entry added mid-iteration?" No. Its iterators reflect the map at some point at or after their creation, and may or may not include later updates. You get no exception and no guarantee.
Get every deep dive in the app
Coming soon to the App StoreComing soon to Google Play