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.
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.
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.
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.
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.
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.
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.
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.
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}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 Pattern | Time Complexity | Memory | Best For |
|---|---|---|---|
iterator.remove() | O(n) shift on ArrayList | O(1) extra | Single removal condition during traversal |
removeIf(predicate) | O(n) | O(1) extra | Simple predicate, Java 8+ |
| Collect then modify | O(n) traversal + O(k) changes | O(k) for temp list | Complex conditions, both add and remove |
| Descending index loop | O(n) | O(1) extra | ArrayList-specific, no iterator needed |
new ArrayList<>(original) | O(n) copy | O(n) for copy | Read-only iteration, modify original separately |
| Switch to concurrent collection | O(n) per write for COWAL | O(n) copy | Multi-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
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
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
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
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-modifyInterview 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
| Topic | Link |
|---|---|
| How Iterator's modCount check is the mechanism behind ConcurrentModificationException | Java Iterator → |
| How fail-fast and fail-safe iterators differ and which collections use each | Java Collections Framework → |
| How ArrayList's internal Itr class implements the modCount fail-fast check | Java ArrayList → |
| How HashMap's entrySet iterator triggers CME on structural map modification | Java HashMap → |
| How Java Streams avoid explicit iterator management in most traversal scenarios | Java Streams API → |