Java Tutorial
🔍

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.

Java
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.

Java
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.
Java
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.
Java
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

FeatureFail-fastFail-safe
Throws CME?Yes — on structural change during iterationNo — never
Works onLive collection dataSnapshot or weakly consistent view
CollectionsArrayList, HashMap, HashSet, TreeMap, etc.CopyOnWriteArrayList, ConcurrentHashMap, etc.
Packagejava.utiljava.util.concurrent
Sees new elements added during iteration?Throws before seeing themCOWAL: No. CHM: possibly
Memory overheadMinimal — no copyCOWAL: O(n) copy per write. CHM: minimal
Write costNo extra costCOWAL: O(n). CHM: O(1) avg
Thread-safe iteration?No — only for bugs in single-threaded codeYes
Use caseSingle-threaded, defensive bug detectionMulti-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.

Java
1// File: WebhookListener.java 2 3@FunctionalInterface 4public interface WebhookListener { 5 void onEvent(String eventType, String payload); 6}
Java
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}
Java
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

AspectFail-fast CollectionsCopyOnWriteArrayListConcurrentHashMap
Read (iterate)O(n) — no lock, direct arrayO(n) — no lock, snapshot arrayO(n) — no lock, weakly consistent
Write during iterationNot allowed — CMEO(n) — copies full arrayO(1) avg — bucket-level
Iterator creationO(1) — snapshot modCountO(1) — snapshot array referenceO(1)
Memory per writeNoneO(n) — full copyO(1)
Sees latest writes?Yes (or CME)No — snapshotPossibly
Thread-safe readsNoYesYes
Thread-safe writesNoYesYes

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

Java
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 all

Mistake 2 — Using CopyOnWriteArrayList for Frequently Written Lists

Java
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

Java
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 forward

Mistake 4 — Expecting Fail-fast to Guarantee Thread Safety

Java
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 synchronisation

Interview 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 CopyOnWriteArrayListadd(), 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

TopicLink
How Iterator's modCount mechanism is the foundation of fail-fast behaviourJava Iterator →
How ArrayList's internal modCount field drives ConcurrentModificationExceptionJava ArrayList →
How HashMap implements the same fail-fast modCount mechanism for Map iterationJava HashMap →
How the Collections Framework organises fail-fast and concurrent collectionsJava Collections Framework →
How Java Streams provide an alternative to manual iterator loopsJava Streams API →
Java Fail-fast vs Fail-safe Iterator | DevStackFlow