Definition
- Fail-fast (ArrayList, HashMap, HashSet): iterator checks
modCounton everynext()call; if the collection was structurally modified (add/remove) by anything other thaniterator.remove(), it throws CME - Fail-safe (ConcurrentHashMap, CopyOnWriteArrayList): iterators never throw CME; CopyOnWriteArrayList iterates a snapshot, ConcurrentHashMap uses weakly consistent traversal
- modCount mechanism: every structural modification increments
modCount; the iterator storesexpectedModCountat creation and verifies on each step - iterator.remove(): increments both
modCountandexpectedModCount, keeping them in sync
Fail-fast is like a bank clerk counting cash in the vault. If anyone opens the vault door during the count, the clerk stops immediately: "Someone touched the vault!" (CME). This catches problems early. Fail-safe is like counting a photocopy of the vault ledger. Even if someone adds cash to the real vault, the clerk continues counting the photocopy peacefully. No alarm, but the count might be slightly outdated.
Understanding the Concepts
What is fail-fast vs fail-safe iteration?
Fail-fast and fail-safe describe how iterators respond when the underlying collection is modified during iteration.
| Feature | Fail-fast | Fail-safe (weakly consistent) |
|---|---|---|
| Collections | ArrayList, HashMap, HashSet, LinkedList | ConcurrentHashMap, CopyOnWriteArrayList |
| Throws CME | Yes (on structural modification) | Never |
| Mechanism | modCount check on every next() | Snapshot or tolerant traversal |
| Reflects changes | N/A (throws before seeing them) | May or may not see concurrent changes |
| Memory overhead | None | COWAL: full array copy; CHM: none |
| Thread safety | Not thread-safe | Thread-safe |
package com.bank.demo;
import java.util.*;
import java.util.concurrent.*;
public class FailFastVsFailSafeDemo {
public static void main(String[] args) {
// FAIL-FAST: ArrayList
List<String> accounts = new ArrayList<>(List.of("SAV2001", "CLOSED-001", "CUR3001"));
try {
for (String acc : accounts) {
if (acc.startsWith("CLOSED")) {
accounts.remove(acc); // CME!
}
}
} catch (ConcurrentModificationException e) {
System.out.println("ArrayList: " + e.getClass().getSimpleName());
}
// FAIL-SAFE: ConcurrentHashMap
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("SAV2001", "Active");
map.put("CLOSED-001", "Closed");
map.put("CUR3001", "Active");
for (var entry : map.entrySet()) {
if (entry.getValue().equals("Closed")) {
map.remove(entry.getKey()); // no CME!
}
}
System.out.println("ConcurrentHashMap after: " + map);
// FAIL-SAFE: CopyOnWriteArrayList
List<String> cowl = new CopyOnWriteArrayList<>(List.of("SAV2001", "CLOSED-001", "CUR3001"));
for (String acc : cowl) {
if (acc.startsWith("CLOSED")) {
cowl.remove(acc); // no CME! Iterator uses snapshot.
}
}
System.out.println("COWAL after: " + cowl);
}
}
ArrayList: ConcurrentModificationException
ConcurrentHashMap after: {SAV2001=Active, CUR3001=Active}
COWAL after: [SAV2001, CUR3001]
How does the modCount mechanism work?
Every ArrayList, HashMap, and HashSet maintains an internal modCount field. Every structural modification (add, remove, clear) increments this counter. When an iterator is created, it stores the current modCount as expectedModCount. On every call to next(), the iterator checks:
// Simplified from OpenJDK ArrayList.Itr
public E next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// ... return next element
}
public void remove() {
// ... remove the element
expectedModCount = ++modCount; // keep in sync!
}
iterator.remove() is safe because it increments BOTH modCount and expectedModCount, keeping them synchronized.
What are the safe ways to remove during iteration?
package com.bank.demo;
import java.util.*;
public class SafeRemovalDemo {
public static void main(String[] args) {
List<String> accounts = new ArrayList<>(List.of("SAV2001", "CLOSED-001", "CUR3001", "CLOSED-002"));
// Method 1: Iterator.remove()
Iterator<String> it = accounts.iterator();
while (it.hasNext()) {
if (it.next().startsWith("CLOSED")) {
it.remove();
}
}
System.out.println("Iterator.remove: " + accounts);
// Reset for next demo
accounts = new ArrayList<>(List.of("SAV2001", "CLOSED-001", "CUR3001", "CLOSED-002"));
// Method 2: removeIf (Java 8) – cleanest
accounts.removeIf(acc -> acc.startsWith("CLOSED"));
System.out.println("removeIf: " + accounts);
// Method 3: Collect and remove after
accounts = new ArrayList<>(List.of("SAV2001", "CLOSED-001", "CUR3001", "CLOSED-002"));
List<String> toRemove = new ArrayList<>();
for (String acc : accounts) {
if (acc.startsWith("CLOSED")) toRemove.add(acc);
}
accounts.removeAll(toRemove);
System.out.println("removeAll: " + accounts);
// Method 4: Stream filter (create new list)
accounts = new ArrayList<>(List.of("SAV2001", "CLOSED-001", "CUR3001", "CLOSED-002"));
List<String> active = accounts.stream()
.filter(acc -> !acc.startsWith("CLOSED"))
.toList();
System.out.println("Stream filter: " + active);
}
}
Iterator.remove: [SAV2001, CUR3001]
removeIf: [SAV2001, CUR3001]
removeAll: [SAV2001, CUR3001]
Stream filter: [SAV2001, CUR3001]
What are common mistakes?
Mistake: Assuming CME is only thrown in multi-threaded code
// WRONG assumption: "I have one thread, so no CME"
for (String s : list) {
list.remove(s); // CME! Even in single-threaded code!
}
// CME is about structural modification during iteration,
// not about multiple threads.
What should a senior engineer know?
- CME is best-effort. The fail-fast behavior is not guaranteed under all circumstances. It should not be relied upon for correctness – it is a debugging aid, not a contract.
- Maps have the same behavior. HashMap.entrySet().iterator() is fail-fast. ConcurrentHashMap.entrySet().iterator() is weakly consistent.
- removeIf on Maps. You can use map.entrySet().removeIf(e -> e.getValue() < threshold) to safely remove map entries during iteration.
- for-each is syntactic sugar for Iterator. for (T t : collection) compiles to Iterator. That is why collection.remove() during for-each causes CME.
What are common interview follow-up questions?
- "What causes ConcurrentModificationException?" – Structural modification (add/remove) during iteration, detected via modCount.
- "Is CME only in multi-threaded code?" – No, it happens in single-threaded code when modifying during for-each.
- "How do you safely remove during iteration?" – iterator.remove() or removeIf().
- "What is weakly consistent?" – Iterator may or may not reflect concurrent modifications.
Visual Overview
Fail-fast mechanism (ArrayList): list.iterator() → expectedModCount = modCount (e.g., 5) Iteration: next() → check: modCount == 5? YES → return element next() → check: modCount == 5? YES → return element list.remove(x) → modCount = 6 next() → check: modCount(6) == expectedModCount(5)? NO → CME! iterator.remove(): remove() → modCount = 6 AND expectedModCount = 6 → in sync → safe! Fail-safe (CopyOnWriteArrayList): iterator() → captures snapshot of internal array list.add(x) → creates NEW array (old snapshot unchanged) next() → reads from snapshot → no CME, but won't see new element
Key Takeaway
Fail-fast iterators catch bugs early by throwing ConcurrentModificationException when the collection is modified during iteration. The mechanism is simple: the iterator stores the expected modCount at creation and checks it on every next() call. If they differ, it throws CME. This happens even in single-threaded code – it is about structural modification during iteration, not about multiple threads.
Fail-safe iterators in ConcurrentHashMap and CopyOnWriteArrayList never throw CME. They trade accuracy for safety: CopyOnWriteArrayList's iterator works on a frozen snapshot, and ConcurrentHashMap's iterator tolerates concurrent modifications with weakly consistent semantics.
In the National Bank of India's batch processing system, Rajesh uses removeIf(acc -> acc.getStatus().equals("CLOSED")) to safely filter closed accounts from the active list. It is cleaner than manual iterator.remove() loops and avoids the CME trap entirely.
Remember one sentence: for-each + collection.remove() throws CME; use iterator.remove() or removeIf() instead.
---