Java Fail-fast vs Fail-safe Iterator
Java Fail-fast vs Fail-safe Iterator
When a Java collection is modified while an iterator is scanning it, two completely different things can happen depending on the collection: either the iterator immediately throws ConcurrentModificationException — that is fail-fast behaviour — or it continues silently without any error — that is fail-safe behaviour. Which one fires, and why, depends entirely on the collection you are using and how it implements its iterator.
Getting this wrong in production causes two classes of bugs: unexpected exceptions that crash a thread mid-traversal, or silent data inconsistencies where the iterator processes stale or incomplete data. Both are real bugs. Understanding the distinction lets you choose the right collection for the access pattern before the bug appears.
What Are Fail-fast and Fail-safe Iterators?
Fail-fast iterator: An iterator that throws ConcurrentModificationException immediately when it detects that the collection was structurally modified outside the iterator during an active traversal. It detects this via an internal modCount counter. The goal is to surface the bug as early as possible — a structural modification during iteration almost always means the code has a logic error.
Fail-safe iterator: An iterator that does not throw on structural modification. It either works on a frozen snapshot of the collection taken at iterator creation time, or uses a weakly consistent traversal strategy that tolerates concurrent changes. The goal is uninterrupted traversal even in the presence of concurrent modification.
The diagram below shows both types side by side.
FAIL-FAST (ArrayList, HashMap, HashSet, TreeMap, TreeSet...)
Thread or same code: Iterator:
list.add("new element") next() call
| |
modCount++ (now 42) check: modCount == expectedModCount?
| |
(list state changed) 42 != 41 ← MISMATCH
|
throw ConcurrentModificationException
FAIL-SAFE — CopyOnWriteArrayList:
Thread A: list.add("X") Thread B: iterator running
| |
copy array → new array iterates original snapshot
swap reference never sees "X"
| |
(no CME — thread B has snapshot) completes safely
FAIL-SAFE — ConcurrentHashMap:
Thread A: map.put("key", val) Thread B: iterator running
| |
updates one bucket may or may not see "key"
| |
(no CME — weakly consistent) completes safely
The key distinction is not about thread safety per se — a single thread can trigger CME in fail-fast collections by calling list.add() inside a for-each loop. The thread count is irrelevant. What matters is whether the collection was structurally modified while an iterator was active.
Which Collections Use Which Behaviour
FAIL-FAST COLLECTIONS (throw ConcurrentModificationException): java.util package — all standard collections: ArrayList, LinkedList, Vector, Stack HashSet, LinkedHashSet, TreeSet HashMap, LinkedHashMap, TreeMap, Hashtable ArrayDeque, PriorityQueue FAIL-SAFE COLLECTIONS (no ConcurrentModificationException): java.util.concurrent package — concurrent collections: CopyOnWriteArrayList → full array snapshot at iterator creation CopyOnWriteArraySet → backed by CopyOnWriteArrayList, same behaviour ConcurrentHashMap → weakly consistent — may miss recent puts/removes ConcurrentLinkedQueue → weakly consistent ConcurrentSkipListMap → weakly consistent ConcurrentSkipListSet → weakly consistent Also fail-safe: Collections.unmodifiableXxx() wrappers — CME not possible because structural modification throws UnsupportedOperationException first
How the Fail-fast Mechanism Works Internally
Every standard Java collection maintains an int modCount field. This counter starts at 0 and increments on every structural modification — every add(), remove(), clear(), set() that changes the collection's size or structure.
ARRAYLIST'S modCount TRACKING:
ArrayList internal fields:
Object[] elementData — the backing array
int size — number of live elements
int modCount — starts at 0, increments on structural change
Actions that increment modCount:
add(E) → modCount++
add(int, E) → modCount++
remove(int) → modCount++
remove(Object) → modCount++
clear() → modCount++
sort() → modCount++
removeIf() → modCount++ per removal (handled internally)
Actions that do NOT increment modCount:
get(int) — read-only, no structural change
set(int, E) — replaces, size unchanged, no structural change
contains(Object) — read-only
size() — read-only
Iterator creation (iterator()):
int cursor = 0
int lastRet = -1
int expectedModCount = list.modCount ← snapshot taken HERE
Every next() call:
if (modCount != expectedModCount)
throw new ConcurrentModificationException()
...proceed with element access
The snapshot is taken once at iterator creation. Between creation and exhaustion, if any structural change increments modCount, the mismatch is detected on the next next() call and CME fires.
Core Examples — Fail-fast in Action
Triggering ConcurrentModificationException
The most common CME pattern appears in fresher code: modifying a list inside a for-each loop. The for-each compiles to an iterator, and the direct list.remove() increments modCount without updating the iterator's expectedModCount.
1// File: FailFastDemo.java
2
3import java.util.ArrayList;
4import java.util.ConcurrentModificationException;
5import java.util.Iterator;
6import java.util.List;
7
8public class FailFastDemo {
9
10 public static void main(String[] args) {
11
12 List<String> orders = new ArrayList<>();
13 orders.add("ORD-001"); orders.add("ORD-002");
14 orders.add("ORD-003"); orders.add("ORD-004");
15
16 System.out.println("=== Triggering CME (wrong approach) ===");
17 try {
18 for (String order : orders) {
19 System.out.println(" Processing: " + order);
20 if ("ORD-002".equals(order)) {
21 orders.remove(order); // increments modCount — NOT synced to iterator
22 }
23 }
24 } catch (ConcurrentModificationException e) {
25 System.out.println(" CME thrown at next next() after modCount change");
26 }
27
28 System.out.println("\nList state after CME: " + orders);
29 // ORD-002 was removed before CME — partial modification is the danger
30 // The list is in an inconsistent processed state
31
32 System.out.println();
33
34 // Single-thread CME — no concurrent threads needed
35 List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
36 Iterator<Integer> it = numbers.iterator();
37 it.next(); // returns 1 — modCount == expectedModCount at this point
38 numbers.add(99); // modCount++ — expectedModCount is now stale
39 System.out.println("=== CME after external add() ===");
40 try {
41 it.next(); // modCount (6) != expectedModCount (5) — throws immediately
42 } catch (ConcurrentModificationException e) {
43 System.out.println(" CME fired: external add() invalidated the iterator");
44 }
45 }
46}Output:
=== Triggering CME (wrong approach) ===
Processing: ORD-001
Processing: ORD-002
CME thrown at next next() after modCount change
List state after CME: [ORD-001, ORD-003, ORD-004]
=== CME after external add() ===
CME fired: external add() invalidated the iterator
Safe Removal — The Correct Patterns
Three patterns fix the CME issue, each appropriate for a different situation.
1// File: SafeRemovalDemo.java
2
3import java.util.ArrayList;
4import java.util.Iterator;
5import java.util.List;
6
7public class SafeRemovalDemo {
8
9 public static void main(String[] args) {
10
11 // Pattern 1: iterator.remove() — updates expectedModCount after removal
12 List<String> list1 = new ArrayList<>(
13 List.of("PENDING", "DELIVERED", "PENDING", "CANCELLED", "PENDING")
14 );
15 Iterator<String> it = list1.iterator();
16 while (it.hasNext()) {
17 if ("PENDING".equals(it.next())) {
18 it.remove(); // safe — iterator syncs modCount internally
19 }
20 }
21 System.out.println("Pattern 1 (iterator.remove): " + list1);
22
23 // Pattern 2: removeIf — Java 8+ one-liner, handles modCount internally
24 List<String> list2 = new ArrayList<>(
25 List.of("PENDING", "DELIVERED", "PENDING", "CANCELLED", "PENDING")
26 );
27 list2.removeIf("PENDING"::equals);
28 System.out.println("Pattern 2 (removeIf) : " + list2);
29
30 // Pattern 3: collect into a new list, then clear and addAll
31 List<String> list3 = new ArrayList<>(
32 List.of("PENDING", "DELIVERED", "PENDING", "CANCELLED", "PENDING")
33 );
34 List<String> toKeep = new ArrayList<>();
35 for (String status : list3) {
36 if (!"PENDING".equals(status)) {
37 toKeep.add(status);
38 }
39 }
40 list3.clear();
41 list3.addAll(toKeep);
42 System.out.println("Pattern 3 (collect + replace): " + list3);
43 }
44}Output:
Pattern 1 (iterator.remove): [DELIVERED, CANCELLED]
Pattern 2 (removeIf) : [DELIVERED, CANCELLED]
Pattern 3 (collect + replace): [DELIVERED, CANCELLED]
How Fail-safe Works — CopyOnWriteArrayList
CopyOnWriteArrayList takes a fundamentally different approach. Every write operation — add(), remove(), set() — copies the entire backing array, performs the modification on the copy, and atomically swaps the reference. The iterator holds a reference to the array at the time it was created. Any subsequent writes create a new array and have no effect on the iterator's snapshot.
CopyOnWriteArrayList INTERNAL STRUCTURE:
Write path (add, remove, set):
1. Lock acquired (ReentrantLock)
2. Copy current array to newArray
3. Modify newArray
4. Atomically set array = newArray (volatile write)
5. Lock released
Iterator creation:
Iterator snapshot = current array reference (volatile read)
— iterator will always see THIS array, never a newer one
Read path (iterator next()):
No lock — reads from snapshot array directly
Never checks modCount — it does not exist
Never throws CME — snapshot is immutable from iterator's view
CONSEQUENCE:
Iterators are completely isolated from writes.
A thread calling add() gets a new array.
The iterator's array is untouched.
Iteration cost: O(1) per element, no locks.
Write cost: O(n) — full array copy every time.
1// File: FailSafeCOWALDemo.java
2
3import java.util.Iterator;
4import java.util.List;
5import java.util.concurrent.CopyOnWriteArrayList;
6
7public class FailSafeCOWALDemo {
8
9 public static void main(String[] args) {
10
11 CopyOnWriteArrayList<String> eventListeners = new CopyOnWriteArrayList<>();
12 eventListeners.add("EmailNotifier");
13 eventListeners.add("SMSNotifier");
14 eventListeners.add("PushNotifier");
15
16 System.out.println("=== Fail-safe: no CME when list changes during iteration ===");
17 Iterator<String> it = eventListeners.iterator(); // snapshot taken here
18
19 // Modify the list while the iterator is active — no CME
20 eventListeners.add("SlackNotifier"); // creates a NEW array; snapshot unchanged
21
22 System.out.println("Listeners in snapshot (iterator sees 3, not 4):");
23 while (it.hasNext()) {
24 System.out.println(" " + it.next());
25 }
26
27 System.out.println("\nCurrent list (has 4 after add):");
28 eventListeners.forEach(l -> System.out.println(" " + l));
29
30 System.out.println();
31
32 // Simulating concurrent modification — same thread, illustrates snapshot isolation
33 CopyOnWriteArrayList<String> activeUsers = new CopyOnWriteArrayList<>(
34 List.of("user-priya", "user-rohan", "user-ananya")
35 );
36 Iterator<String> userIt = activeUsers.iterator(); // snapshot: 3 users
37 activeUsers.remove("user-rohan"); // modifies live list
38 activeUsers.add("user-karan"); // adds to live list
39
40 System.out.println("=== Snapshot isolation — iterator sees original state ===");
41 while (userIt.hasNext()) {
42 System.out.println(" " + userIt.next()); // user-rohan still appears
43 }
44 System.out.println("Live list: " + activeUsers); // rohan gone, karan added
45 }
46}Output:
=== Fail-safe: no CME when list changes during iteration ===
Listeners in snapshot (iterator sees 3, not 4):
EmailNotifier
SMSNotifier
PushNotifier
Current list (has 4 after add):
EmailNotifier
SMSNotifier
PushNotifier
SlackNotifier
=== Snapshot isolation — iterator sees original state ===
user-priya
user-rohan
user-ananya
Live list: [user-priya, user-ananya, user-karan]
How Fail-safe Works — ConcurrentHashMap
ConcurrentHashMap takes a different approach to fail-safe than CopyOnWriteArrayList. It does not snapshot the entire map — instead, it uses weakly consistent iteration. The iterator is allowed to reflect some, all, or none of the concurrent modifications that happen after it is created.
ConcurrentHashMap ITERATOR — WEAKLY CONSISTENT:
Properties:
- Never throws ConcurrentModificationException
- No modCount field — structural modifications are not tracked
- May see modifications made after iterator creation, or may not
- Guarantees: will traverse every entry that existed at creation
and may or may not traverse entries added after creation
Contrast with CopyOnWriteArrayList:
COWAL: always sees exactly the state at iterator creation
CHM: may see a mix of old and new state
Why weakly consistent?
CHM splits the map into many segments/buckets.
Each bucket can be locked independently.
The iterator scans bucket by bucket.
Buckets scanned before a write: see old state.
Buckets scanned after the write: see new state.
No global snapshot needed — efficient for large maps.
1// File: FailSafeCHMDemo.java
2
3import java.util.Map;
4import java.util.concurrent.ConcurrentHashMap;
5
6public class FailSafeCHMDemo {
7
8 public static void main(String[] args) {
9
10 ConcurrentHashMap<String, Integer> productStock = new ConcurrentHashMap<>();
11 productStock.put("Laptop", 45);
12 productStock.put("Mouse", 120);
13 productStock.put("Keyboard", 80);
14 productStock.put("Monitor", 25);
15
16 System.out.println("=== ConcurrentHashMap — no CME on structural modification ===");
17
18 // Modifying the map during for-each — no exception thrown
19 for (Map.Entry<String, Integer> entry : productStock.entrySet()) {
20 System.out.println(" " + entry.getKey() + " = " + entry.getValue());
21 if ("Mouse".equals(entry.getKey())) {
22 productStock.put("Webcam", 60); // structural modification — no CME
23 }
24 }
25
26 System.out.println("\nFinal map: " + productStock);
27 // "Webcam" may or may not appear during the loop — weakly consistent
28 // It will be in the final map regardless
29
30 System.out.println();
31
32 // forEach with compute — safe concurrent accumulation
33 ConcurrentHashMap<String, Integer> wordCount = new ConcurrentHashMap<>();
34 String[] words = {"java", "spring", "java", "hibernate", "java", "spring"};
35
36 for (String word : words) {
37 wordCount.merge(word, 1, Integer::sum); // atomic merge — thread-safe
38 }
39 System.out.println("Word counts: " + wordCount);
40 }
41}Output:
=== ConcurrentHashMap — no CME on structural modification ===
Laptop = 45
Mouse = 120
Keyboard = 80
Monitor = 25
Final map: {Laptop=45, Mouse=120, Keyboard=80, Monitor=25, Webcam=60}
Word counts: {spring=2, java=3, hibernate=1}
Fail-fast vs Fail-safe — Direct Comparison
| Feature | Fail-fast | Fail-safe |
|---|---|---|
| Throws CME? | Yes — on structural change during iteration | No — never |
| Works on | Live collection data | Snapshot or weakly consistent view |
| Collections | ArrayList, HashMap, HashSet, TreeMap, etc. | CopyOnWriteArrayList, ConcurrentHashMap, etc. |
| Package | java.util | java.util.concurrent |
| Sees new elements added during iteration? | Throws before seeing them | COWAL: No. CHM: possibly |
| Memory overhead | Minimal — no copy | COWAL: O(n) copy per write. CHM: minimal |
| Write cost | No extra cost | COWAL: O(n). CHM: O(1) avg |
| Thread-safe iteration? | No — only for bugs in single-threaded code | Yes |
| Use case | Single-threaded, defensive bug detection | Multi-threaded, concurrent access |
Real-World Example — Razorpay Webhook Event Dispatcher
A webhook event dispatcher handles incoming payment events. Multiple threads register and deregister listeners, while the dispatcher thread continuously iterates registered listeners to deliver events. With a fail-fast ArrayList, deregistering a listener during dispatch throws CME and crashes the dispatch loop. CopyOnWriteArrayList solves this correctly — writes create a new array and the in-progress dispatch completes on the stable snapshot.
1// File: WebhookListener.java
2
3@FunctionalInterface
4public interface WebhookListener {
5 void onEvent(String eventType, String payload);
6}1// File: EventDispatcher.java
2
3import java.util.List;
4import java.util.concurrent.CopyOnWriteArrayList;
5
6public class EventDispatcher {
7
8 // CopyOnWriteArrayList: multiple threads can register/deregister
9 // without disrupting the dispatch loop running on the dispatcher thread
10 private final CopyOnWriteArrayList<WebhookListener> listeners =
11 new CopyOnWriteArrayList<>();
12
13 public void register(WebhookListener listener) {
14 listeners.add(listener);
15 System.out.println(" Registered listener. Total: " + listeners.size());
16 }
17
18 public boolean deregister(WebhookListener listener) {
19 boolean removed = listeners.remove(listener);
20 System.out.println(" Deregistered listener. Total: " + listeners.size());
21 return removed;
22 }
23
24 // Dispatch to all listeners in the current snapshot
25 // Any register/deregister during this call affects the next dispatch, not this one
26 public void dispatch(String eventType, String payload) {
27 System.out.println(" Dispatching [" + eventType + "] to "
28 + listeners.size() + " listeners (snapshot):");
29 for (WebhookListener listener : listeners) {
30 // If another thread calls register() or deregister() here,
31 // CopyOnWriteArrayList creates a new array — this loop is unaffected
32 listener.onEvent(eventType, payload);
33 }
34 }
35
36 public int listenerCount() { return listeners.size(); }
37}1// File: WebhookDispatcherDemo.java
2
3public class WebhookDispatcherDemo {
4
5 public static void main(String[] args) throws InterruptedException {
6
7 EventDispatcher dispatcher = new EventDispatcher();
8
9 // Register three listeners
10 WebhookListener emailAlert = (type, payload) ->
11 System.out.println(" EMAIL : " + type + " — " + payload);
12 WebhookListener smsAlert = (type, payload) ->
13 System.out.println(" SMS : " + type + " — " + payload);
14 WebhookListener dashboardUpdate = (type, payload) ->
15 System.out.println(" DASHBOARD: " + type + " — " + payload);
16
17 System.out.println("=== Registering listeners ===");
18 dispatcher.register(emailAlert);
19 dispatcher.register(smsAlert);
20 dispatcher.register(dashboardUpdate);
21
22 System.out.println("\n=== First dispatch (3 listeners) ===");
23 dispatcher.dispatch("PAYMENT_SUCCESS", "txn_id=TXN-9821, amount=1299.00");
24
25 // Simulate deregistering SMS during operation
26 System.out.println("\n=== Deregistering SMS listener ===");
27 dispatcher.deregister(smsAlert);
28
29 System.out.println("\n=== Second dispatch (2 listeners in live list) ===");
30 dispatcher.dispatch("PAYMENT_FAILED", "txn_id=TXN-9822, reason=INSUFFICIENT_FUNDS");
31
32 // With ArrayList this would have thrown CME if deregister happened
33 // during dispatch from another thread. CopyOnWriteArrayList makes it safe.
34 System.out.println("\nFinal listener count: " + dispatcher.listenerCount());
35 }
36}Output:
=== Registering listeners ===
Registered listener. Total: 1
Registered listener. Total: 2
Registered listener. Total: 3
=== First dispatch (3 listeners) ===
Dispatching [PAYMENT_SUCCESS] to 3 listeners (snapshot):
EMAIL : PAYMENT_SUCCESS — txn_id=TXN-9821, amount=1299.00
SMS : PAYMENT_SUCCESS — txn_id=TXN-9821, amount=1299.00
DASHBOARD: PAYMENT_SUCCESS — txn_id=TXN-9821, amount=1299.00
=== Deregistering SMS listener ===
Deregistered listener. Total: 2
=== Second dispatch (2 listeners in live list) ===
Dispatching [PAYMENT_FAILED] to 2 listeners (snapshot):
EMAIL : PAYMENT_FAILED — txn_id=TXN-9822, reason=INSUFFICIENT_FUNDS
DASHBOARD: PAYMENT_FAILED — txn_id=TXN-9822, reason=INSUFFICIENT_FUNDS
Final listener count: 2
Performance Considerations
| Aspect | Fail-fast Collections | CopyOnWriteArrayList | ConcurrentHashMap |
|---|---|---|---|
| Read (iterate) | O(n) — no lock, direct array | O(n) — no lock, snapshot array | O(n) — no lock, weakly consistent |
| Write during iteration | Not allowed — CME | O(n) — copies full array | O(1) avg — bucket-level |
| Iterator creation | O(1) — snapshot modCount | O(1) — snapshot array reference | O(1) |
| Memory per write | None | O(n) — full copy | O(1) |
| Sees latest writes? | Yes (or CME) | No — snapshot | Possibly |
| Thread-safe reads | No | Yes | Yes |
| Thread-safe writes | No | Yes | Yes |
When write cost matters: CopyOnWriteArrayList copies the entire array on every write. For a list of 100,000 elements that changes frequently, this creates heavy GC pressure. Use it only for read-heavy, write-rare scenarios — event listener registries, configuration snapshots, observer lists.
When consistency matters: ConcurrentHashMap is weakly consistent — an iterator that starts before a put() may or may not see the new entry. For use cases that require a consistent snapshot, copy the map: new HashMap<>(concurrentHashMap) and iterate the copy.
Best Practices
Use fail-fast collections for all single-threaded code. ArrayList, HashMap, and HashSet are faster than their concurrent counterparts because they have no locking or copying overhead. Their fail-fast behaviour is a safety net — it surfaces the code-level mistake of modifying a collection during iteration immediately, rather than letting a silent data inconsistency propagate.
Use CopyOnWriteArrayList only for read-heavy, write-rare lists. The right scenario is: many threads read and iterate frequently, writes happen infrequently (registering or deregistering a listener, loading a configuration at startup). The O(n) copy cost is acceptable when writes are rare. For write-heavy concurrent lists, use Collections.synchronizedList(new ArrayList<>()) or a ConcurrentLinkedDeque instead.
Use ConcurrentHashMap as the default concurrent map. For any Map accessed from multiple threads, ConcurrentHashMap is the correct default. It is significantly faster than Collections.synchronizedMap() under concurrent load because it locks at bucket granularity rather than on the entire map. The weakly consistent iterator is acceptable for most use cases — if a strict snapshot is needed, copy the map before iterating.
Never catch CME and continue. ConcurrentModificationException is a signal that the code has a structural bug — a collection is being modified during iteration in a way that violates the iteration contract. Wrapping the iteration in a try-catch and catching CME to continue is a code smell. Fix the root cause: use removeIf(), iterator.remove(), or the right concurrent collection.
Common Mistakes
Mistake 1 — Catching CME and Continuing Silently
1List<String> orders = new ArrayList<>(List.of("A", "B", "C"));
2
3// WRONG — swallowing CME hides a bug and leaves the iterator in undefined state
4try {
5 for (String order : orders) {
6 orders.remove(order); // triggers CME
7 }
8} catch (ConcurrentModificationException e) {
9 // ignoring it — partial removal happened, list is in inconsistent state
10 System.out.println("Loop done with some removals");
11}
12
13// CORRECT — fix the root cause
14orders.removeIf(order -> true); // removes allMistake 2 — Using CopyOnWriteArrayList for Frequently Written Lists
1// WRONG — each add() copies the entire 50,000-element array
2CopyOnWriteArrayList<LogEntry> highFrequencyLog = new CopyOnWriteArrayList<>();
3// If add() is called 1,000 times per second, this copies 50MB+ per second
4
5// For high-write, concurrent access: use ConcurrentLinkedDeque or
6// a synchronized ArrayList with a read lock pattern
7List<LogEntry> betterLog = Collections.synchronizedList(new ArrayList<>());Mistake 3 — Assuming ConcurrentHashMap Iterator Sees All Recent Changes
1ConcurrentHashMap<String, Integer> liveMap = new ConcurrentHashMap<>();
2liveMap.put("A", 1); liveMap.put("B", 2); liveMap.put("C", 3);
3
4Iterator<String> keyIt = liveMap.keySet().iterator();
5liveMap.put("D", 4); // D may or may not appear during iteration — weakly consistent
6
7List<String> snapshot = new ArrayList<>();
8while (keyIt.hasNext()) {
9 snapshot.add(keyIt.next()); // "D" may be missing
10}
11
12// If a consistent snapshot is required:
13List<String> consistentSnapshot = new ArrayList<>(liveMap.keySet());
14// This copies at a point in time — weakly consistent like the map itself,
15// but the copy is immutable from this point forwardMistake 4 — Expecting Fail-fast to Guarantee Thread Safety
1List<String> sharedList = new ArrayList<>();
2// Thread 1 adds elements, Thread 2 iterates
3
4// WRONG assumption: CME means the code is safe
5// CME means the BUG was detected — it does NOT mean the data is consistent
6// Data corruption in the backing array can still occur before CME fires
7// because ArrayList is not thread-safe
8
9// CORRECT — use a thread-safe collection from the start
10List<String> safeList = new CopyOnWriteArrayList<>();
11// OR protect the entire read-write critical section with synchronisationInterview Questions
Q1. What is the difference between fail-fast and fail-safe iterators in Java?
A fail-fast iterator throws ConcurrentModificationException immediately when it detects a structural modification to the collection outside the iterator during traversal. It achieves this by tracking a modCount counter — the collection increments modCount on every structural change, and the iterator snapshots it at creation. On every next() call, the iterator checks whether modCount still matches. A fail-safe iterator does not throw — it either works on a snapshot of the collection at creation time (CopyOnWriteArrayList) or uses weakly consistent traversal (ConcurrentHashMap). Fail-fast collections are in java.util; fail-safe ones are in java.util.concurrent.
Q2. What exactly is modCount and which classes use it?
modCount is an int field maintained by most java.util collection classes — ArrayList, LinkedList, HashMap, HashSet, TreeMap, TreeSet, ArrayDeque, and others. It starts at 0 and increments on every structural modification: every add(), remove(), clear(), and similar operations that change the collection's size or structure. set() on a List does not increment modCount because it does not change structure. Iterator classes snapshot modCount as expectedModCount at creation time and compare on every next() call. If they differ, ConcurrentModificationException is thrown.
Q3. Does ConcurrentModificationException always mean a multi-threading problem?
No — and this is a common misconception. Despite the name, ConcurrentModificationException fires whenever a collection is structurally modified while an iterator is active, even in completely single-threaded code. The most common cause is calling list.remove(element) inside a for-each loop on the same list — one thread, one loop, no concurrent threads. The word "concurrent" means simultaneous modification and traversal, not multi-threaded execution. Multi-threaded structural modification is one scenario; the single-thread for-each mistake is a more common one.
Q4. How does CopyOnWriteArrayList achieve fail-safe iteration?
Every write operation on CopyOnWriteArrayList — add(), remove(), set(), clear() — acquires a ReentrantLock, creates a full copy of the backing array, applies the modification to the copy, then atomically swaps the internal array reference to the new copy via a volatile write. The iterator stores a reference to the array at the time it was created — this reference never changes for the iterator's lifetime. Any subsequent writes create new arrays, leaving the iterator's snapshot untouched. The iterator never checks modCount and never throws CME. The tradeoff is that every write is O(n).
Q5. When would you choose ConcurrentHashMap over Collections.synchronizedMap()?
Collections.synchronizedMap() wraps a HashMap with a single mutex — every method acquires the same lock, so only one thread executes any operation at a time. ConcurrentHashMap uses fine-grained locking at the bucket level — multiple threads can read without any lock, and writers lock only the specific bucket being modified. For high-concurrency workloads with many concurrent reads, ConcurrentHashMap is significantly faster. synchronizedMap() is simpler and appropriate for low-concurrency scenarios where simplicity outweighs the performance difference. ConcurrentHashMap also provides atomic operations like putIfAbsent() and computeIfAbsent() that are genuinely atomic — synchronizedMap() methods are individually atomic but not composable.
Q6. What is the correct way to remove elements from a collection during iteration?
Three approaches in order of preference: (1) collection.removeIf(predicate) — the cleanest Java 8+ approach, handles modCount bookkeeping internally, works with any Collection, and expresses intent clearly. (2) Explicit iterator.remove() — call it.next() first, then it.remove() — the iterator updates expectedModCount after the removal, keeping it in sync. (3) Collect indices or values to remove, finish the iteration, then perform removals in a separate pass. Never call list.remove() or list.add() directly inside a for-each loop on the same collection.
FAQs
Does fail-fast mean the iterator is thread-safe?
No. Fail-fast means the iterator detects a structural modification and throws an exception — it does not prevent the modification from happening. In multi-threaded code, two threads can simultaneously corrupt the backing array before the iterator even gets to check modCount. CME is best-effort detection in single-threaded code. For actual thread safety, use collections from java.util.concurrent.
Can a CopyOnWriteArrayList iterator see elements added during iteration?
No. The iterator holds a reference to the array snapshot taken at creation time. Elements added after creation go into a new array, which the iterator never sees. This is by design — the snapshot guarantee makes iteration predictable. If you need to process newly added elements, create a new iterator after the additions are complete.
What is the difference between weakly consistent and strongly consistent iteration on ConcurrentHashMap?
Weakly consistent means the iterator reflects the state of the map at some point between its creation and the current moment — it may or may not see puts and removes made after the iterator was created, but it will not miss entries that existed at creation time and were not removed. Strongly consistent would require a global snapshot lock that blocks all writers during iteration. ConcurrentHashMap trades strong consistency for throughput — no global lock means iteration and modification happen concurrently at full speed.
Is removeIf fail-safe or fail-fast?
removeIf() is internally fail-fast for standard collections like ArrayList and HashSet — it uses the iterator mechanism and handles modCount bookkeeping correctly, so calling removeIf() from one thread while another thread modifies the same collection can still throw CME or produce incorrect results. removeIf() is safe within a single thread. For multi-threaded scenarios, use CopyOnWriteArrayList.removeIf() or ConcurrentHashMap.entrySet().removeIf(), which handle concurrent access correctly.
What happens if CME fires mid-loop — is the collection in a valid state?
The collection itself is in a valid structural state after CME — the modification that caused the CME was completed successfully (the add() or remove() that triggered it actually happened). What is invalid is the iterator's view — it can no longer be used safely. The collection can be iterated again with a fresh iterator. The concern is that the modification may have been partial from the business logic perspective — for example, half the elements may have been processed before CME fired.
Is it safe to iterate a HashMap from two different threads at the same time?
No — even read-only iteration on a HashMap from two threads is not safe if any thread is writing to it concurrently. A write in progress can leave the internal bucket array in a partially updated state, causing the reading thread to see corrupted data or throw NullPointerException. For multi-threaded use, use ConcurrentHashMap whose iteration is designed for concurrent access. For truly read-only use after a build phase, wrap with Collections.unmodifiableMap() — writes are then blocked by UnsupportedOperationException.
Summary
Fail-fast and fail-safe describe two opposite philosophies in how Java collection iterators handle structural modification during traversal. Fail-fast — the default for all java.util collections — uses a modCount counter to detect structural changes and throws ConcurrentModificationException immediately. This is a bug-detection mechanism, not a thread-safety guarantee. Fail-safe — used by CopyOnWriteArrayList and the concurrent collections in java.util.concurrent — allows traversal to continue either via snapshot isolation or weakly consistent traversal.
The practical rules: fix CME at the root by using removeIf() or iterator.remove() rather than catching the exception; use CopyOnWriteArrayList for event listener registries and other read-heavy lists that change rarely; use ConcurrentHashMap as the default concurrent map. The choice of fail-fast vs fail-safe is ultimately the choice of collection class — it is not a setting you configure, it is a property of the implementation.
For interviews: know that CME can fire in single-threaded code, explain the modCount mechanism precisely, distinguish CopyOnWriteArrayList's snapshot isolation from ConcurrentHashMap's weakly consistent iteration, and always demonstrate fixing CME at the root rather than catching it.
What to Read Next
| Topic | Link |
|---|---|
| How Iterator's modCount mechanism is the foundation of fail-fast behaviour | Java Iterator → |
| How ArrayList's internal modCount field drives ConcurrentModificationException | Java ArrayList → |
| How HashMap implements the same fail-fast modCount mechanism for Map iteration | Java HashMap → |
| How the Collections Framework organises fail-fast and concurrent collections | Java Collections Framework → |
| How Java Streams provide an alternative to manual iterator loops | Java Streams API → |