Java Tutorial
🔍

Java ConcurrentModificationException

Java ConcurrentModificationException

ConcurrentModificationException is one of the most frequently encountered runtime exceptions in Java, and also one of the most misunderstood. Despite having "concurrent" in its name, it fires just as often in completely single-threaded code. The exception exists for one reason: to tell you immediately that a collection was structurally modified while an iterator was actively traversing it — something that produces undefined, unpredictable results if allowed to continue silently.

Understanding exactly what triggers it, what does not, and how to fix each case is non-negotiable for any Java developer working with collections.

What Is ConcurrentModificationException?

java.util.ConcurrentModificationException is an unchecked runtime exception — it extends RuntimeException and the compiler never requires you to declare or catch it. It is thrown by the iterator of a collection when that collection is structurally modified outside the iterator while the iterator is active.

EXCEPTION HIERARCHY:

java.lang.Throwable
    └── java.lang.Exception
            └── java.lang.RuntimeException
                    └── java.util.ConcurrentModificationException  ← unchecked

PACKAGE: java.util
SINCE:   Java 1.2

KEY FACTS:
  - Unchecked — no try-catch required, no throws declaration needed
  - Signals a programming error, not a recoverable condition
  - Thrown by fail-fast iterators in java.util package collections
  - NOT thrown by fail-safe iterators in java.util.concurrent package
  - Can fire in single-threaded code — no multiple threads required
  - "Best-effort" detection — JVM spec does not guarantee it fires every time

What "Structural Modification" Means

A structural modification is any operation that changes the number of elements in the collection — any add(), remove(), clear(), or addAll(). It does NOT include operations that only replace an element's value, like List.set(index, newValue) — those are not structural because the collection's size does not change.

STRUCTURAL MODIFICATIONS (increment modCount → may trigger CME):
  list.add(element)
  list.add(index, element)
  list.remove(index)
  list.remove(object)
  list.addAll(collection)
  list.removeAll(collection)
  list.retainAll(collection)
  list.clear()
  list.sort()               ← uses modCount to track reordering

NOT STRUCTURAL (do NOT increment modCount → safe during iteration):
  list.get(index)
  list.set(index, newValue) ← replaces, does not change size
  list.contains(object)
  list.size()
  list.isEmpty()
  iterator.remove()         ← safe because iterator syncs modCount itself
  list.removeIf(predicate)  ← safe because handles modCount internally

The modCount Mechanism — How CME Is Detected

Every java.util collection that supports fail-fast iteration maintains an int field called modCount. This counter starts at 0 and increments on every structural modification.

When an iterator is created via collection.iterator(), it snapshots the current modCount as its own expectedModCount. Every next() call begins with one check:

MODCOUNT CHECK ON EVERY next() CALL:

  if (modCount != expectedModCount)
      throw new ConcurrentModificationException()

Example — ArrayList with 3 elements (modCount = 3):

  Iterator created:
    cursor = 0
    expectedModCount = 3   ← snapshot taken here

  Iteration step 1:
    modCount (3) == expectedModCount (3)  ← OK
    returns element at index 0
    cursor = 1

  External add() called:
    modCount becomes 4     ← snapshot is now stale

  Iteration step 2:
    modCount (4) != expectedModCount (3)  ← MISMATCH
    throw ConcurrentModificationException  ← fires immediately

IMPORTANT: The exception fires on the NEXT next() call after the
modification — NOT at the moment of modification.

The modCount check is described in the Java specification as "best effort" — the JVM does not guarantee CME fires on every structural modification. In practice it fires reliably for standard collection implementations, but you should never write code that relies on CME firing to detect correctness problems.

When ConcurrentModificationException Fires

Scenario 1 — Direct Modification Inside for-each Loop

The most common CME pattern in production code. The for-each loop compiles to an iterator, and calling list.remove() directly increments modCount without updating the iterator's expectedModCount.

Java
1// File: CMEScenario1Demo.java 2 3import java.util.ArrayList; 4import java.util.ConcurrentModificationException; 5import java.util.List; 6 7public class CMEScenario1Demo { 8 9 public static void main(String[] args) { 10 11 List<String> statuses = new ArrayList<>(); 12 statuses.add("PENDING"); statuses.add("DELIVERED"); 13 statuses.add("PENDING"); statuses.add("RETURNED"); 14 statuses.add("PENDING"); 15 16 System.out.println("=== CME from list.remove() inside for-each ==="); 17 try { 18 for (String status : statuses) { 19 System.out.println(" Visiting: " + status); 20 if ("PENDING".equals(status)) { 21 statuses.remove(status); // modCount++ but iterator.expectedModCount unchanged 22 } 23 } 24 } catch (ConcurrentModificationException e) { 25 System.out.println(" CME fired after modification detected"); 26 } 27 System.out.println(" List after partial modification: " + statuses); 28 // Partial state: first PENDING was removed, then CME fired 29 System.out.println(); 30 31 // CME from add() inside for-each 32 List<String> tags = new ArrayList<>(List.of("java", "spring", "kafka")); 33 System.out.println("=== CME from list.add() inside for-each ==="); 34 try { 35 for (String tag : tags) { 36 System.out.println(" Visiting: " + tag); 37 if ("spring".equals(tag)) { 38 tags.add("hibernate"); // structural modification during traversal 39 } 40 } 41 } catch (ConcurrentModificationException e) { 42 System.out.println(" CME fired after add()"); 43 } 44 System.out.println(" Tags after partial modification: " + tags); 45 } 46}
Output:
=== CME from list.remove() inside for-each ===
  Visiting: PENDING
  Visiting: DELIVERED
  CME fired after modification detected
  List after partial modification: [DELIVERED, PENDING, RETURNED, PENDING]

=== CME from list.add() inside for-each ===
  Visiting: java
  Visiting: spring
  CME fired after add()
  Tags after partial modification: [java, spring, kafka, hibernate]

Scenario 2 — External Modification Between Iterator Calls

CME does not require a for-each loop. Any structural modification to a collection after an iterator is created but before the iterator is exhausted will trigger it.

Java
1// File: CMEScenario2Demo.java 2 3import java.util.ArrayList; 4import java.util.ConcurrentModificationException; 5import java.util.HashMap; 6import java.util.Iterator; 7import java.util.List; 8import java.util.Map; 9 10public class CMEScenario2Demo { 11 12 public static void main(String[] args) { 13 14 // CME between iterator creation and use 15 List<Integer> prices = new ArrayList<>(List.of(100, 200, 300, 400, 500)); 16 Iterator<Integer> it = prices.iterator(); 17 it.next(); // returns 100 — OK so far 18 19 prices.add(600); // structural change after iterator was created 20 System.out.println("=== CME from modification between next() calls ==="); 21 try { 22 it.next(); // modCount mismatch — throws CME 23 } catch (ConcurrentModificationException e) { 24 System.out.println(" CME fired — modCount changed between next() calls"); 25 } 26 27 System.out.println(); 28 29 // CME during Map iteration — modifying the map's keySet 30 Map<String, Integer> inventory = new HashMap<>(); 31 inventory.put("Laptop", 10); inventory.put("Mouse", 50); 32 inventory.put("Keyboard", 25); inventory.put("Monitor", 8); 33 34 System.out.println("=== CME from map.put() during entrySet iteration ==="); 35 try { 36 for (Map.Entry<String, Integer> entry : inventory.entrySet()) { 37 System.out.println(" Processing: " + entry.getKey()); 38 if ("Mouse".equals(entry.getKey())) { 39 inventory.put("Webcam", 15); // structural change during iteration 40 } 41 } 42 } catch (ConcurrentModificationException e) { 43 System.out.println(" CME fired during Map iteration"); 44 } 45 System.out.println(" Inventory now: " + inventory); 46 } 47}
Output:
=== CME from modification between next() calls ===
  CME fired — modCount changed between next() calls

=== CME from map.put() during entrySet iteration ===
  Processing: Laptop
  Processing: Mouse
  CME fired during Map iteration
  Inventory now: {Laptop=10, Mouse=50, Keyboard=25, Monitor=8, Webcam=15}

Every Safe Fix Pattern

Fix 1 — iterator.remove() for Removal During Traversal

The iterator's own remove() method is the only structurally safe removal during traversal. It removes the last element returned by next() and updates expectedModCount to match the new modCount internally.

Java
1// File: CMEFix1IteratorRemove.java 2 3import java.util.ArrayList; 4import java.util.Iterator; 5import java.util.List; 6 7public class CMEFix1IteratorRemove { 8 9 public static void main(String[] args) { 10 11 List<String> orders = new ArrayList<>( 12 List.of("ORD-001:PENDING", "ORD-002:DELIVERED", "ORD-003:PENDING", 13 "ORD-004:CANCELLED", "ORD-005:PENDING") 14 ); 15 System.out.println("Before: " + orders); 16 17 // iterator.remove() — safe because it syncs expectedModCount after removal 18 Iterator<String> it = orders.iterator(); 19 while (it.hasNext()) { 20 String order = it.next(); 21 if (order.contains("PENDING")) { 22 it.remove(); // removes last returned element — modCount sync happens here 23 } 24 } 25 System.out.println("After removing PENDING (iterator.remove): " + orders); 26 } 27}
Output:
Before: [ORD-001:PENDING, ORD-002:DELIVERED, ORD-003:PENDING, ORD-004:CANCELLED, ORD-005:PENDING]
After removing PENDING (iterator.remove): [ORD-002:DELIVERED, ORD-004:CANCELLED]

Fix 2 — removeIf() for Predicate-Based Removal (Java 8+)

Collection.removeIf(predicate) is the cleanest modern approach. It handles modCount bookkeeping internally, eliminates the explicit iterator boilerplate, and expresses intent clearly.

Java
1// File: CMEFix2RemoveIf.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class CMEFix2RemoveIf { 7 8 public static void main(String[] args) { 9 10 List<Integer> scores = new ArrayList<>(List.of(88, 42, 95, 15, 76, 33, 91, 28)); 11 System.out.println("Before: " + scores); 12 13 // removeIf — no explicit iterator, no CME risk 14 scores.removeIf(score -> score < 50); 15 System.out.println("After removeIf (< 50): " + scores); 16 17 // Works on Map views too 18 java.util.Map<String, Integer> stock = new java.util.HashMap<>(); 19 stock.put("Laptop", 5); stock.put("Mouse", 0); stock.put("Keyboard", 12); 20 stock.put("Monitor", 0); stock.put("Webcam", 3); 21 22 System.out.println("\nStock before: " + stock); 23 stock.entrySet().removeIf(entry -> entry.getValue() == 0); // remove out-of-stock 24 System.out.println("Stock after removing zero-qty: " + stock); 25 } 26}
Output:
Before: [88, 42, 95, 15, 76, 33, 91, 28]
After removeIf (< 50): [88, 95, 76, 91]

Stock before: {Laptop=5, Keyboard=12, Monitor=0, Mouse=0, Webcam=3}
Stock after removing zero-qty: {Laptop=5, Keyboard=12, Webcam=3}

Fix 3 — Collect First, Modify After

When both adding and removing during traversal is needed, or when the removal logic is complex, collect the items to change in a separate list, finish the traversal, then apply all changes.

Java
1// File: CMEFix3CollectThenModify.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class CMEFix3CollectThenModify { 7 8 public static void main(String[] args) { 9 10 List<String> products = new ArrayList<>( 11 List.of("Laptop-5G", "Mouse-Wireless", "Keyboard-Basic", 12 "Monitor-4K", "Laptop-i5", "Webcam-HD") 13 ); 14 15 // Collect items to remove and items to add — then apply outside the loop 16 List<String> toRemove = new ArrayList<>(); 17 List<String> toAdd = new ArrayList<>(); 18 19 for (String product : products) { 20 if (product.startsWith("Laptop")) { 21 toRemove.add(product); // collect for removal 22 toAdd.add(product.replace("Laptop", "MacBook")); // collect replacement 23 } 24 } 25 26 // Apply changes after traversal — no active iterator at this point 27 products.removeAll(toRemove); 28 products.addAll(toAdd); 29 30 System.out.println("After replacement: " + products); 31 } 32}
Output:
After replacement: [Mouse-Wireless, Keyboard-Basic, Monitor-4K, Webcam-HD, MacBook-5G, MacBook-i5]

Fix 4 — Standard Index Loop for ArrayList

For ArrayList, a descending index loop avoids CME entirely because it does not use an iterator — it uses direct array index access. Traversing backward means index shifts caused by removal do not affect elements you have not yet visited.

Java
1// File: CMEFix4IndexLoop.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class CMEFix4IndexLoop { 7 8 public static void main(String[] args) { 9 10 List<String> notifications = new ArrayList<>( 11 List.of("INFO: Login", "ERROR: DB Timeout", "INFO: Logout", 12 "ERROR: NPE in OrderService", "WARN: High memory", "ERROR: 500") 13 ); 14 System.out.println("Before: " + notifications); 15 16 // Descending index loop — no iterator, no modCount, no CME 17 // Descending ensures indices of unvisited elements (lower indices) are stable 18 for (int i = notifications.size() - 1; i >= 0; i--) { 19 if (notifications.get(i).startsWith("ERROR")) { 20 notifications.remove(i); // shifts elements at higher indices (already visited) 21 } 22 } 23 System.out.println("After removing ERROR logs: " + notifications); 24 25 System.out.println(); 26 27 // Ascending index loop is risky — skips elements after removal 28 List<Integer> values = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6)); 29 for (int i = 0; i < values.size(); i++) { 30 if (values.get(i) % 2 == 0) { 31 values.remove(i); // index i shifts — next element at i is skipped 32 i--; // compensate by stepping back — necessary fix 33 } 34 } 35 System.out.println("After ascending loop with i-- compensation: " + values); 36 } 37}
Output:
Before: [INFO: Login, ERROR: DB Timeout, INFO: Logout, ERROR: NPE in OrderService, WARN: High memory, ERROR: 500]
After removing ERROR logs: [INFO: Login, INFO: Logout, WARN: High memory]

After ascending loop with i-- compensation: [1, 3, 5]

Fix 5 — Use Concurrent Collections for Multi-threaded Code

When multiple threads access the same collection and at least one writes, the fix is not an iteration pattern — it is choosing the right collection from java.util.concurrent.

Java
1// File: CMEFix5ConcurrentCollections.java 2 3import java.util.List; 4import java.util.Map; 5import java.util.concurrent.ConcurrentHashMap; 6import java.util.concurrent.CopyOnWriteArrayList; 7 8public class CMEFix5ConcurrentCollections { 9 10 public static void main(String[] args) { 11 12 // CopyOnWriteArrayList — fail-safe for read-heavy, write-rare lists 13 CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>(); 14 listeners.add("EmailService"); listeners.add("SMSService"); listeners.add("PushService"); 15 16 // No CME — each write copies the array, iterator works on original snapshot 17 for (String listener : listeners) { 18 System.out.println(" Notifying: " + listener); 19 if ("SMSService".equals(listener)) { 20 listeners.remove("PushService"); // modifies list during iteration — safe 21 } 22 } 23 System.out.println("Listeners after: " + listeners); 24 25 System.out.println(); 26 27 // ConcurrentHashMap — fail-safe for concurrent Map access 28 ConcurrentHashMap<String, Integer> sessionCount = new ConcurrentHashMap<>(); 29 sessionCount.put("Mumbai", 142); sessionCount.put("Delhi", 89); 30 sessionCount.put("Bengaluru", 211); sessionCount.put("Chennai", 63); 31 32 System.out.println("=== ConcurrentHashMap — no CME on structural modification ==="); 33 for (Map.Entry<String, Integer> entry : sessionCount.entrySet()) { 34 System.out.println(" " + entry.getKey() + ": " + entry.getValue() + " sessions"); 35 sessionCount.put("Hyderabad", 55); // structural change — no CME 36 } 37 System.out.println("Final map: " + sessionCount); 38 } 39}
Output:
  Notifying: EmailService
  Notifying: SMSService
  Notifying: PushService
Listeners after: [EmailService, SMSService]

=== ConcurrentHashMap — no CME on structural modification ===
  Mumbai: 142 sessions
  Delhi: 89 sessions
  Bengaluru: 211 sessions
  Chennai: 63 sessions
Final map: {Mumbai=142, Delhi=89, Bengaluru=211, Chennai=63, Hyderabad=55}

When ConcurrentModificationException Does NOT Fire

Understanding when CME does not fire is as important as knowing when it does.

CME DOES NOT FIRE when:

1. Using fail-safe collections:
   CopyOnWriteArrayList, CopyOnWriteArraySet
   ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentLinkedQueue
   — these never throw CME

2. Using iterator.remove() or iterator.add() (ListIterator):
   — the iterator syncs expectedModCount after its own structural changes

3. Using removeIf(), replaceAll(), sort() on the list directly
   (not during an external iteration):
   — these handle modCount internally

4. Calling List.set(index, value) during iteration:
   — set() is not structural, does not change size, does not increment modCount

5. Reading operations: get(), contains(), size(), isEmpty()
   — read-only, no structural change

6. Iterating a copy of the collection:
   for (String item : new ArrayList<>(originalList)):
   — iterator is on the copy; modifications to original are invisible

Real-World Example — Zepto Order Processing Pipeline

An order processing pipeline at Zepto scans active orders, marks timed-out ones for cancellation, and injects retry entries for failed deliveries. All three operations — scan, remove, and add — happen in a single workflow. Using the wrong approach triggers CME and leaves the order list in a partially modified state.

Java
1// File: Order.java 2 3public class Order { 4 5 private final String orderId; 6 private final String status; 7 private final long createdAtMs; 8 9 public Order(String orderId, String status, long createdAtMs) { 10 this.orderId = orderId; 11 this.status = status; 12 this.createdAtMs = createdAtMs; 13 } 14 15 public String getOrderId() { return orderId; } 16 public String getStatus() { return status; } 17 public long getCreatedAtMs() { return createdAtMs; } 18 19 public boolean isTimedOut(long nowMs, long timeoutMs) { 20 return (nowMs - createdAtMs) > timeoutMs; 21 } 22 23 @Override 24 public String toString() { 25 return String.format("[%s] %-12s", orderId, status); 26 } 27}
Java
1// File: OrderProcessor.java 2 3import java.util.ArrayList; 4import java.util.Iterator; 5import java.util.List; 6 7public class OrderProcessor { 8 9 private static final long TIMEOUT_MS = 30_000L; // 30 seconds 10 11 // Single-pass processor: remove timed-out orders, queue retries for failed ones 12 public static void processOrders(List<Order> orders, long nowMs) { 13 14 List<Order> retryQueue = new ArrayList<>(); // collect new entries first 15 16 // Use iterator.remove() for safe removal during traversal 17 Iterator<Order> it = orders.iterator(); 18 while (it.hasNext()) { 19 Order order = it.next(); 20 21 if (order.isTimedOut(nowMs, TIMEOUT_MS)) { 22 System.out.println(" TIMEOUT : " + order); 23 it.remove(); // safe — iterator manages its own modCount sync 24 } else if ("FAILED".equals(order.getStatus())) { 25 System.out.println(" RETRY : " + order); 26 // Collect retry orders to add AFTER the iteration completes 27 // Do NOT call orders.add() here — that would cause CME 28 retryQueue.add(new Order(order.getOrderId() + "-R1", "RETRYING", 29 System.currentTimeMillis())); 30 it.remove(); // remove the failed order itself 31 } else { 32 System.out.println(" ACTIVE : " + order); 33 } 34 } 35 36 // Apply additions AFTER the traversal — no active iterator at this point 37 orders.addAll(retryQueue); 38 } 39 40 public static void main(String[] args) { 41 42 long now = System.currentTimeMillis(); 43 44 List<Order> orderQueue = new ArrayList<>(); 45 orderQueue.add(new Order("ORD-001", "PROCESSING", now - 10_000)); // 10s old — active 46 orderQueue.add(new Order("ORD-002", "PROCESSING", now - 45_000)); // 45s old — timeout 47 orderQueue.add(new Order("ORD-003", "FAILED", now - 5_000)); // 5s old — retry 48 orderQueue.add(new Order("ORD-004", "PROCESSING", now - 20_000)); // 20s old — active 49 orderQueue.add(new Order("ORD-005", "PROCESSING", now - 60_000)); // 60s old — timeout 50 orderQueue.add(new Order("ORD-006", "FAILED", now - 8_000)); // 8s old — retry 51 52 System.out.println("=== Before processing ==="); 53 orderQueue.forEach(o -> System.out.println(" " + o)); 54 55 System.out.println("\n=== Processing orders ==="); 56 processOrders(orderQueue, now); 57 58 System.out.println("\n=== After processing ==="); 59 orderQueue.forEach(o -> System.out.println(" " + o)); 60 System.out.println("Queue size: " + orderQueue.size()); 61 } 62}
Output:
=== Before processing ===
  [ORD-001] PROCESSING
  [ORD-002] PROCESSING
  [ORD-003] FAILED
  [ORD-004] PROCESSING
  [ORD-005] PROCESSING
  [ORD-006] FAILED

=== Processing orders ===
  ACTIVE   : [ORD-001] PROCESSING
  TIMEOUT  : [ORD-002] PROCESSING
  RETRY    : [ORD-003] FAILED
  ACTIVE   : [ORD-004] PROCESSING
  TIMEOUT  : [ORD-005] PROCESSING
  RETRY    : [ORD-006] FAILED

=== After processing ===
  [ORD-001] PROCESSING
  [ORD-004] PROCESSING
  [ORD-003-R1] RETRYING
  [ORD-006-R1] RETRYING
Queue size: 4

The real-world insight: retry entries are collected in retryQueue during traversal and added with addAll() only after the iterator is exhausted. Calling orders.add() inside the while loop would trigger CME because add() increments modCount while the iterator's expectedModCount remains unchanged.

Performance Considerations

The modCount check in next() is an int comparison — effectively free. It adds no measurable overhead to iteration. The performance question is not about CME detection — it is about which fix pattern to use.

Fix PatternTime ComplexityMemoryBest For
iterator.remove()O(n) shift on ArrayListO(1) extraSingle removal condition during traversal
removeIf(predicate)O(n)O(1) extraSimple predicate, Java 8+
Collect then modifyO(n) traversal + O(k) changesO(k) for temp listComplex conditions, both add and remove
Descending index loopO(n)O(1) extraArrayList-specific, no iterator needed
new ArrayList<>(original)O(n) copyO(n) for copyRead-only iteration, modify original separately
Switch to concurrent collectionO(n) per write for COWALO(n) copyMulti-threaded write-rare

The collect then modify pattern is the most flexible but uses O(k) extra memory for k changes. For very large collections where k is large, iterator.remove() or removeIf() is more memory-efficient.

Best Practices

Treat CME as a programming error to fix, not an exception to catch. ConcurrentModificationException means the code structure is wrong — a collection is being modified during iteration in an unsafe way. Wrapping the iteration in try-catch(ConcurrentModificationException) and continuing leaves the collection in a partially modified state and hides the real bug. The only correct response is to fix the traversal pattern.

Always use removeIf() for simple removal conditions in Java 8+. One line, no iterator boilerplate, handles modCount internally, and clearly expresses intent. list.removeIf(item -> item.getStatus().equals("PENDING")) is always preferable to an explicit iterator loop for straightforward conditions.

Separate the decision to remove from the act of removing. For complex processing where you need to inspect an element fully before deciding — involving accumulated state from earlier elements — the collect-then-modify pattern is the right design. Decide during traversal, remove after. This is cleaner than forcing all logic into a removeIf predicate.

Never add elements to a collection directly inside a for-each loop on that collection. Adding elements while iterating a fail-fast collection always triggers CME. Use ListIterator.add() for in-place insertion during traversal on a List, or collect additions and addAll() after the loop.

Common Mistakes

Mistake 1 — Catching CME and Continuing

Java
1List<String> items = new ArrayList<>(List.of("A", "B", "C", "D", "E")); 2 3// WRONG — catching CME hides the bug and leaves items in a partial state 4try { 5 for (String item : items) { 6 if ("C".equals(item)) { 7 items.remove(item); // CME fires on next next() call 8 } 9 } 10} catch (ConcurrentModificationException ignored) { 11 // Never swallow CME — data is in undefined state here 12} 13// items is now ["A", "B", "D", "E"] — "C" removed, but was anything else skipped? 14 15// CORRECT — fix the pattern 16items.removeIf("C"::equals);

Mistake 2 — Removing by Value on a List of Integers Using the Wrong Overload

Java
1List<Integer> ids = new ArrayList<>(List.of(101, 102, 103, 104, 105)); 2 3// WRONG — remove(102) calls remove(int index) — removes element AT index 102 4// which throws IndexOutOfBoundsException for small lists 5// This is a separate bug, but it comes up alongside CME discussions 6ids.remove(102); // IndexOutOfBoundsException — not CME, but confuses beginners 7 8// CORRECT — remove by value must box the int 9ids.remove(Integer.valueOf(102)); // removes the value 102 10System.out.println(ids); // [101, 103, 104, 105]

Mistake 3 — Assuming set() is Safe When It Should Not Be

Java
1List<String> names = new ArrayList<>(List.of("alice", "bob", "charlie")); 2Iterator<String> it = names.iterator(); 3it.next(); // "alice" 4names.set(0, "ALICE"); // set() does NOT increment modCount — still safe so far 5 6it.next(); // "bob" — no CME here (set is not structural) 7 8names.add("dave"); // structural — increments modCount 9try { 10 it.next(); // CME fires — add() changed modCount 11} catch (ConcurrentModificationException e) { 12 System.out.println("CME fired after add(), not after set()"); 13}
Output:
CME fired after add(), not after set()

Mistake 4 — Confusing CME with Multi-threading When the Bug Is Single-threaded

Java
1// This is single-threaded — but still triggers CME 2List<String> products = new ArrayList<>(List.of("A", "B", "C")); 3 4for (String product : products) { 5 helper(products, product); // CME fires here — not because of threads 6} 7 8static void helper(List<String> list, String item) { 9 if ("B".equals(item)) { 10 list.remove(item); // modifies the list — same thread, same iterator, CME 11 } 12} 13 14// The fix is the same regardless of single or multi-threaded: 15// use removeIf, iterator.remove(), or collect-then-modify

Interview Questions

Q1. What is ConcurrentModificationException in Java and when does it occur?

ConcurrentModificationException is an unchecked RuntimeException thrown by fail-fast iterators when they detect that the collection was structurally modified outside the iterator while the iterator was actively traversing it. It is detected via a modCount field — the collection increments modCount on every structural change, and the iterator checks whether its expectedModCount still matches on every next() call. It fires in single-threaded code just as readily as multi-threaded code — the most common cause is calling list.remove() or list.add() inside a for-each loop on the same list. It does not guarantee correctness if ignored — the collection is in a partially modified, potentially inconsistent state after CME fires.

Q2. Why is CME described as "best effort" in the Java specification?

The Java specification explicitly states that fail-fast iterators operate on a "best-effort basis" and the implementation does not guarantee that CME fires on every concurrent structural modification. This means: in practice, CME fires reliably for standard JDK implementations, but a program should never rely on CME detection to implement correct logic or thread safety. The spec permits implementations that do not track modCount or track it imprecisely. Code that swallows CME or uses its absence as a signal that no modification occurred is always incorrect.

Q3. What is the difference between iterator.remove() and list.remove() during iteration?

list.remove(element) increments the collection's modCount but does not update the iterator's expectedModCount. The next iterator.next() call detects the mismatch and throws CME. iterator.remove() calls the backing collection's removal method, which also increments modCount, but then immediately sets expectedModCount = modCount — keeping the iterator's snapshot in sync with the new state. This is why iterator.remove() is structurally safe: the iterator explicitly acknowledges the change it caused and updates itself accordingly.

Q4. Does List.set() trigger ConcurrentModificationException?

No. List.set(int index, E element) replaces an existing element without changing the collection's size. It does not increment modCount in standard implementations like ArrayList and LinkedList. The iterator's expectedModCount check passes because modCount was not changed. CME is only triggered by structural modifications — operations that add or remove elements and change the collection's size. set() is safe to call on a list while an iterator is active.

Q5. How does removeIf() avoid ConcurrentModificationException?

Collection.removeIf(predicate) internally iterates the collection and calls iterator.remove() for each matching element, then updates expectedModCount after each removal. Because it manages the iterator state internally, the iteration remains consistent. In Java 11+, ArrayList.removeIf() uses an optimised implementation that does not use an external iterator at all — it uses index-based scanning with a bitmap of elements to remove, then shifts elements in a single pass. Either way, the caller does not need to manage the iterator, and CME cannot be triggered by the removal logic.

Q6. What is the correct approach when you need to both remove and add elements during a single list traversal?

The safest pattern is to separate the decision from the modification: during traversal with an iterator, identify elements to remove (use iterator.remove()) and collect new elements in a separate temporary list. After the iterator is exhausted, call list.addAll(tempList) to apply the additions. list.addAll() after iteration is safe because no iterator is active at that point. Alternatively, ListIterator.add() can safely insert elements during traversal on a List. Direct list.add() inside a for-each loop on the same list always triggers CME.

FAQs

Can ConcurrentModificationException occur without using multiple threads?

Yes — and this is the most common misconception. The "concurrent" in the name refers to concurrent modification and iteration, not concurrent threads. Calling list.remove() inside a for-each loop in a completely single-threaded program triggers CME. No second thread is involved. The exception fires whenever the collection's modCount no longer matches the iterator's expectedModCount, regardless of how many threads are running.

Does Collections.synchronizedList() prevent ConcurrentModificationException?

No. Collections.synchronizedList() wraps a list with a mutex that serialises individual method calls, but iteration is not atomic — the lock is released between individual next() calls. If another thread calls add() or remove() on the wrapped list between two next() calls on the iterator, CME fires. To iterate safely, you must synchronise the entire iteration block: synchronized(syncList) { for (String s : syncList) {...} }. For iteration-safe concurrent access without manual synchronisation, use CopyOnWriteArrayList.

What happens to the collection state when CME fires?

The modification that caused CME has already completed successfully — the add() or remove() that incremented modCount ran to completion before CME was thrown. What is invalid is the iterator's state — it can no longer be used after CME. The collection itself is structurally sound and can be iterated again with a fresh iterator. The concern is the business logic: if the loop was processing elements and some were handled before CME fired while others were not, the processing is incomplete and the application state may be inconsistent.

Is CME thrown from Set and Map as well as List?

Yes. CME fires from HashSet, LinkedHashSet, TreeSet, HashMap, LinkedHashMap, and TreeMap iterators under the same conditions as ArrayList. Any structural modification to the collection while an iterator obtained from it is active triggers CME on the next next() call. For maps, structural modifications include put() on new keys, remove(), and clear(). Calling entry.setValue() via Map.Entry during entrySet() iteration does not increment modCount and is safe.

Does using a Stream avoid ConcurrentModificationException?

No — standard Collection.stream() creates a Spliterator backed by the collection. Modifying the backing collection while processing the stream can trigger CME or produce incorrect results. The stream source is the live collection. To safely process and modify, collect results to a new list and apply changes after the stream operation completes. CopyOnWriteArrayList.stream() is safe for concurrent modification.

What is the best way to handle ConcurrentModificationException in production code?

Do not catch it — fix the root cause. The correct responses are: use removeIf() for simple conditional removal, use iterator.remove() for removal during traversal, collect changes and apply after iteration for complex cases, or switch to a concurrent collection for multi-threaded scenarios. Logging CME and retrying the iteration is never a correct solution — the retry will trigger CME again under the same conditions unless the underlying pattern is fixed.

Summary

ConcurrentModificationException is a fail-fast iterator's immediate signal that a collection was structurally modified outside the iterator during traversal. It is detected via a modCount counter that every java.util collection maintains — the iterator snapshots modCount at creation and verifies it on every next() call. A mismatch means a structural change happened, and CME fires.

The word "concurrent" is misleading — single-threaded code triggers it just as easily when list.remove() or list.add() is called inside a for-each loop. The fix is never to catch and ignore it. The correct fix depends on the situation: removeIf() for simple predicate removal, iterator.remove() for conditional removal during traversal, collect-then-modify for complex scenarios, or a concurrent collection for multi-threaded access.

For interviews: know that CME can fire single-threaded, explain modCount and expectedModCount precisely, distinguish iterator.remove() from list.remove() and why one is safe while the other is not, and describe the collect-then-modify pattern for the case where both removal and addition are needed in one pass.

What to Read Next

TopicLink
How Iterator's modCount check is the mechanism behind ConcurrentModificationExceptionJava Iterator →
How fail-fast and fail-safe iterators differ and which collections use eachJava Collections Framework →
How ArrayList's internal Itr class implements the modCount fail-fast checkJava ArrayList →
How HashMap's entrySet iterator triggers CME on structural map modificationJava HashMap →
How Java Streams avoid explicit iterator management in most traversal scenariosJava Streams API →
Java ConcurrentModificationException | DevStackFlow