> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Java ConcurrentModificationException: Causes and Fixes
- URL: https://www.ggorantala.dev/java-concurrentmodificationexception-causes-and-fixes/
- Published: 2026-08-29T19:11:06.000Z
- Updated: 2026-08-29T19:53:24.000Z
- Description: ConcurrentModificationException usually has nothing to do with threads. Here is exactly what modCount does, why it fires, and the four fixes that actually work.
- Author: Gopi Gorantala
- Tags: Java, java-errors, concurrentmodificationexception, java-collections, java-concurrency, core-java, jvm, java-21

## 1\. The Error

```log
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
	at Repro.main(Repro.java:7)
```

Note the shape: **no message**. `ConcurrentModificationException` almost always arrives with a `null` detail message, and the only useful information is in the frames. The two JDK frames tell you which collection and which access path detected the problem.

The same bug in a `HashMap` looks different:

```log
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1605)
	at java.base/java.util.HashMap$EntryIterator.next(HashMap.java:1638)
	at MapRepro.main(MapRepro.java:6)
```

Through `Collection.forEach` the iterator frames disappear entirely:

```log
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1598)
	at ForEachRepro.main(ForEachRepro.java:6)
```

And through a stream, the detection happens in the spliterator:

```log
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1714)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at StreamRepro.main(StreamRepro.java:8)
```

**Message wording across versions.** The exception message itself has never changed — it is empty. What changed is the *stack trace rendering*: on Java 8 there is no `java.base/` module prefix (`at java.util.ArrayList$Itr.next(ArrayList.java:851)`); from Java 9 onward every JDK frame is prefixed with its module. Line numbers inside `ArrayList.java` differ between builds — the traces above are from OpenJDK 21.0.10\. Do not match on line numbers; match on the frame names.

The Javadoc is explicit that this is not a threading-only exception:

> Note that this exception does not always indicate that an object has been concurrently modified by a *different* thread. If a single thread issues a sequence of method invocations that violates the contract of an object, the object may throw this exception.

## 2\. How to Reproduce It (step-by-step)

Single file, runs with the source launcher on Java 11+ (JEP 330):

```java
import java.util.ArrayList;
import java.util.List;

public class Repro {
    public static void main(String[] args) {
        List<String> orders = new ArrayList<>(List.of("A-1", "A-2", "B-1", "B-2"));
        for (String id : orders) {          // line 7 — the frame in the trace
            if (id.startsWith("A")) {
                orders.remove(id);          // structural modification behind the iterator's back
            }
        }
        System.out.println(orders);
    }
}
```

```bash
java Repro.java
```

On Java 8 (`List.of` does not exist there — use `Arrays.asList` wrapped in an `ArrayList`):

```bash
javac Repro.java && java -cp . Repro
```

### The variant that does *not* throw — and is worse

```java
List<String> l = new ArrayList<>(List.of("a", "b", "c", "d"));
for (String s : l) { if (s.equals("c")) l.remove(s); }   // removes the second-to-last
System.out.println(l);   // prints [a, b, d] — no exception, "d" was never visited
```

This prints `no exception, list = [a, b, d]`. The loop silently skipped an element. Section 5 explains why, and it is the single strongest argument for never relying on this exception to catch the bug for you.

### The multithreaded variant

`Collections.synchronizedList` does **not** save you — it synchronizes each method call, not the iteration:

```java
static final List<Integer> shared = Collections.synchronizedList(new ArrayList<>());
// writer thread: shared.add(i); shared.remove(0);
// main thread:   for (Integer v : shared) sum += v;   // unsynchronized iteration -> CME
```

This one is load- and timing-dependent: it may run for thousands of iterations before throwing, and under a different JIT state or core count it may instead return a silently wrong sum. That non-determinism is the whole point of fail-fast being *best effort*.

### Environment-specific triggers to check

- Only under concurrency: an unsynchronized iteration over a shared `ArrayList`/`HashMap`.
- Only for certain data: removing the second-to-last element never throws (see above).
- Only through a view: `subList`, `keySet`, `entrySet`, `values`, and the Java 21 `reversed()` views are all fail-fast against the backing collection's `modCount`.
- Only in the framework: JPA/Hibernate lazy collections, Spring `ApplicationListener` lists, and Jackson serializers all iterate collections you may be mutating elsewhere.

## 3\. Version Behaviour Matrix

| JDK | Behaviour                                                                                                                                                                                                |
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 8   | Throws. Stack frames have no module prefix. Collection.removeIf and Map.forEach already available (Java 8).                                                                                              |
| 11  | Same semantics. Module prefix java.base/ in traces since 9\. List.of/Map.of immutable factories (JEP 269, Java 9) make the "just don't mutate it" fix easy; Collectors.toUnmodifiableList since Java 10. |
| 17  | Unchanged. Stream.toList() (Java 16) returns an unmodifiable list, which turns a later mutation into UnsupportedOperationException at the mutation site instead of a CME at some distant iteration.      |
| 21  | Unchanged. Sequenced collections (JEP 431) add reversed(), getFirst(), removeFirst() — the reversed views are fail-fast on the same modCount.                                                            |
| 25  | Unchanged (JDK 25 GA, 16 Sep 2025). No JEP has altered fail-fast semantics.                                                                                                                              |

`ConcurrentModificationException` is effectively version-neutral: it has behaved the same since Java 1.2\. What has changed across releases is the *set of APIs that make the mistake unnecessary* — `removeIf` (8), immutable factories (9), `toUnmodifiableList` (10), `Stream.toList` (16). Prefer those over defensive copying.

## 4\. Why It Happens — Surface Level

An enhanced `for` loop over a `Collection` is compiled to an `Iterator` loop. The iterator holds a private cursor into the collection's internal array or node table. When you call `orders.remove(id)` directly on the collection, the collection shifts its elements and changes its size, but the iterator has no idea — its cursor now points at the wrong slot.

Rather than let the loop read stale or shifted data, the JDK's general-purpose collections detect the mismatch and throw. That is what "fail-fast" means: fail quickly and cleanly, instead of returning wrong answers non-deterministically later.

## 5\. Why It Happens — Under the Hood

`AbstractList` declares a `protected transient int modCount`. Every **structural modification** — one that changes the size, or otherwise perturbs the iteration order — increments it. For `HashMap` a structural modification is inserting a new key or removing one; *replacing the value of an existing key is not*, which is why `Map.Entry.setValue` during iteration is legal.

`ArrayList$Itr` snapshots that counter at construction:

```java
int cursor;                        // index of next element to return
int lastRet = -1;                  // index of last element returned; -1 if none
int expectedModCount = modCount;   // snapshot taken when the iterator is created

public boolean hasNext() { return cursor != size; }   // <-- no modCount check

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
```

Three consequences follow directly from this code:

1. **`next()` checks, `hasNext()` does not.** Remove the second-to-last element and `size` drops to `cursor`, so `hasNext()` returns `false`, the loop exits normally, and `checkForComodification()` never runs. You get a silently truncated iteration. This is exactly why the Javadoc says fail-fast operations throw *"on a best-effort basis"* and that the exception *"should be used only to detect bugs."*
2. **`Iterator.remove()` is safe because it re-syncs the snapshot.** It calls the collection's `remove(int)`, then does `expectedModCount = modCount` and fixes `cursor`/`lastRet` itself. Nothing is out of step afterwards.
3. **`modCount` is a plain `int`, not `volatile`.** In the multithreaded case there is no happens-before edge between the writer's increment and the reader's comparison, so the reader may observe a stale `modCount` and *miss* the modification entirely — reading a shifted array instead. Cross-thread CME detection is therefore an accident of memory visibility, not a guarantee. This is the same reason `Collections.synchronizedList` documents that you must hold the wrapper's monitor for the whole iteration.

The bulk operations use the same counter on a different code path. `ArrayList.forEach` reads `modCount` into a local, loops while `modCount == expectedModCount && i < size`, then throws afterwards if it drifted — which is why its stack trace has no `Itr` frames. `ArrayListSpliterator` is *late-binding*: it captures `modCount` at first traversal, not at `stream()` creation, and checks it in `forEachRemaining`/`tryAdvance` — hence the `AbstractPipeline.copyInto` frames when the mutation happens inside a `peek` or `map`.

`ArrayList$SubList` keeps its own `modCount` snapshot and validates on nearly every operation, so mutating the parent list invalidates the view immediately:

```log
at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1497)
at java.base/java.util.ArrayList$SubList.get(ArrayList.java:1232)
```

The concurrent collections take the opposite design. `ConcurrentHashMap`'s iterators are *weakly consistent*: they traverse the bin table as it exists, reflect some updates and not others, and never throw. `CopyOnWriteArrayList`'s iterator holds a reference to the immutable array snapshot taken at creation — writes replace the array, so the iterator simply keeps reading the old one, and `Iterator.remove()` throws `UnsupportedOperationException`.

## 6\. The Fix

**Before**

```java
for (String id : orders) {
    if (id.startsWith("A")) {
        orders.remove(id);
    }
}
```

**After — pick one:**

```java
// 1. Explicit iterator: works on Java 5+, the only option when you also need the element.
for (Iterator<String> it = orders.iterator(); it.hasNext(); ) {
    if (it.next().startsWith("A")) {
        it.remove();
    }
}

// 2. removeIf — Java 8+, the default choice for "remove by predicate".
orders.removeIf(id -> id.startsWith("A"));

// 3. Maps: remove through the entrySet view, not the map.
map.entrySet().removeIf(e -> e.getValue() == 1);

// 4. Don't mutate at all — derive a new collection. Java 16+ for toList().
List<String> kept = orders.stream()
        .filter(id -> !id.startsWith("A"))
        .toList();                       // Java 8: .collect(Collectors.toList())
```

When to use which:

| Situation                                                     | Fix                                                                      |
| ------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Remove by predicate, single collection                        | removeIf — one pass, O(n) on ArrayList, no shifting per removal          |
| Need index, or must also mutate something else                | explicit Iterator \+ it.remove()                                         |
| Result is consumed downstream, source can stay untouched      | stream().filter(...).toList()                                            |
| Must stay Java 7 or the collection is a Map you mutate by key | iterate a defensive copy: for (String k : new ArrayList<>(map.keySet())) |
| Concurrent readers and writers                                | ConcurrentHashMap, or CopyOnWriteArrayList for read-mostly lists         |

Note the performance trap in fix 1: `it.remove()` on an `ArrayList` is O(n) per call because of the `System.arraycopy` shift, so removing half a million elements one at a time is quadratic. `removeIf` on `ArrayList` is a single compacting pass. On `LinkedList` and `HashMap` the iterator removal is O(1).

Also note what is *not* a fix: wrapping in `Collections.synchronizedList` still throws (verified — it is the same `ArrayList$Itr` underneath), and catching `ConcurrentModificationException` and retrying hides a real logic bug behind a race.

## 7\. Best Practices & The Better Design

The durable fix is to stop mutating collections while reading them at all. Treat a collection you are iterating as immutable for the duration of the iteration, and express filtering as a transformation rather than an in-place edit.

```java
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public final class OrderBook {

    private final Map<String, Integer> openQty = new ConcurrentHashMap<>();

    /** Returns a new list; the caller cannot corrupt our state. */
    public List<String> settled(List<String> orderIds) {
        return orderIds.stream()
                .filter(id -> !id.startsWith("A"))
                .toList();                          // unmodifiable, Java 16+
    }

    /** Atomic compound update — no check-then-act, no iteration required. */
    public void fill(String orderId, int qty) {
        openQty.merge(orderId, -qty, (a, b) -> a + b <= 0 ? null : a + b);
    }

    /** Weakly consistent iteration: never throws CME. */
    public Map<String, Integer> snapshot() {
        return Map.copyOf(openQty);                 // Java 10+
    }
}
```

Rules that generalise:

- **Return unmodifiable collections from APIs.** `List.copyOf`, `Map.copyOf`, `Stream.toList` (Java 16+). A caller who mutates now fails at the mutation with `UnsupportedOperationException` — a stack trace that points at the culprit — instead of causing a CME in your iteration much later.
- **Store `ConcurrentHashMap`, not `HashMap`, for anything shared**, and use `compute`/`merge`/`computeIfAbsent` instead of `containsKey` \+ `put`. A non-atomic check-then-act on a `ConcurrentHashMap` is a separate bug class worth its own article.
- **`CopyOnWriteArrayList` only for read-mostly, small lists** — listener registries, config snapshots. Every write copies the whole array.
- **Never expose a mutable internal collection.** Defensive copy on the way in and on the way out.
- **Model "remove while iterating" as filter + replace**, not as mutation. Especially in event-driven code where the same collection is read by a callback.

Related failure modes worth linking from here: broken `equals`/`hashCode` on a key type causes lookup failures in the same `HashMap` you are iterating; `IllegalArgumentException: Comparison method violates its general contract!` is TimSort's equivalent of a contract violation detected mid-operation; and `UnsupportedOperationException` from `Arrays.asList`/`List.of` is what you get when you apply fix 2 to a fixed-size or immutable list.

## 8\. How to Prevent It Long-Term

- **Static analysis in CI.** Error Prone's `ModifyCollectionInEnhancedForLoop` catches the classic case at compile time; `-Xlint:all -Werror` and SpotBugs cover neighbouring collection misuse. IntelliJ flags it as *"Collection is modified during iteration"* — turn that inspection into a CI-enforced rule, not a squiggle developers scroll past.
- **Make it structurally impossible.** Fields typed as `List<T>` and assigned from `List.copyOf(...)`; getters returning `Collections.unmodifiableList(...)`. NullAway-style enforcement of "no mutable collection crosses a module boundary" removes the whole class.
- **Concurrency stress tests.** A JUnit test that spawns one writer and several readers over a shared collection and runs for a few hundred thousand iterations will reliably surface the multithreaded variant. For anything subtle, use `jcstress`. Run these tests with `-XX:-TieredCompilation` occasionally — different JIT states expose different interleavings.
- **Read the trace, don't guess.** `ArrayList$Itr` → an enhanced-`for` or explicit iterator. `ArrayList.forEach` → a lambda that mutated its own source. `ArrayListSpliterator.forEachRemaining` → a stream whose pipeline mutated the source. `SubList.checkForComodification` → a view outlived its parent's modification.
- **When it only reproduces in production**, capture it with JFR rather than guessing: `jcmd <pid> JFR.start name=cme settings=profile` and inspect `jdk.JavaExceptionThrow` events, which record the throw site even when the application swallows the exception.
- **Team convention:** any `for (X x : collection)` body containing `collection.` is a review block. It is a two-character grep.

## 9\. Key Takeaways

- `ConcurrentModificationException` is usually **single-threaded** — it means "you mutated a collection while iterating it", not "you have a race".
- It is driven by one non-volatile `int`, `modCount`, snapshotted by the iterator and compared in `next()`. `hasNext()` never checks it, so **removing the second-to-last element silently truncates the loop instead of throwing**.
- Fail-fast is **best effort**. Never write code whose correctness depends on this exception being thrown, and never catch-and-retry it.
- The fixes, in order of preference: `removeIf` → explicit `Iterator.remove()` → `stream().filter().toList()` → iterate a defensive copy. `Collections.synchronizedList` is not a fix.
- `ConcurrentHashMap` iterators are weakly consistent and `CopyOnWriteArrayList` iterators are snapshots — neither throws, which is why they belong in shared state and `HashMap`/`ArrayList` do not.