Fix: Java ConcurrentModificationException
Part of: Java & JVM Errors
Quick Answer
How to fix Java ConcurrentModificationException caused by modifying a collection while iterating, HashMap concurrent access, stream operations, and multi-threaded collection usage.
The Misleading Exception
Personally, I think ConcurrentModificationException has the worst name in the entire Java standard library. I have watched junior developers spend an hour adding synchronized blocks to fix this, when they had a single-threaded program. The exception is a safety net for collection-mutation-during-iteration, and it fires regardless of how many threads are involved. You run a Java program and get:
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
at com.example.Main.process(Main.java:15)Or with HashMap:
java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1597)Despite the name, this exception does not require multiple threads. It occurs when you modify a collection (add, remove, or replace elements) while iterating over it with an iterator or enhanced for-loop.
Quick Reference Before You Dive In
If you arrived here from Google with a fresh stack trace, the five facts that resolve roughly 90 percent of cases:
- The name lies. This is almost never about multiple threads. It is a fail-fast detector for mutation-during-iteration, single-threaded or not. The Java
ConcurrentModificationExceptionJavadoc and theIteratorinterface docs are the canonical references. - The fix in the for-each loop is
removeIf(predicate)(Java 8+). It is the cleanest one-liner, works on anyCollection, and is fail-fast-safe by design. Reach for it before any other fix. - In a manual
whileloop, useIterator.remove(), NOTlist.remove(). The iterator updates the modification count internally so the fail-fast check does not trigger. stream().forEach(... source.remove(...))is always wrong. Streams are for transformation, not mutation. Usefilter().collect()to produce a new collection instead.- If the bug really IS multi-threaded, switch to
CopyOnWriteArrayList(read-heavy) orConcurrentHashMap(general). Addingsynchronizedblocks does not fix iteration-time fail-fast detection inside a single thread.
The rest of this article walks through each fix in detail, plus the failure modes most other guides skip.
What “Fail-Fast” Actually Means Here
Java’s iterators are fail-fast. They track a modification count on the underlying collection. Each time the collection is structurally modified (elements added or removed), the count increments. When the iterator’s next() method detects that the count changed since iteration started, it throws ConcurrentModificationException.
This is a safety mechanism. Without it, the iterator might skip elements, visit elements twice, or throw an IndexOutOfBoundsException.
Common causes:
- Removing elements in a for-each loop. The most common cause by far.
- Adding elements during iteration. Inserting into a list or map while looping over it.
- Multiple threads modifying a non-thread-safe collection. Two threads accessing the same
ArrayListorHashMap. - Stream operations modifying the source. Using
.forEach()on a stream and modifying the original collection. - Nested iteration with modification. Modifying the outer collection from the inner loop.
Worth understanding precisely: the modification count check happens inside Iterator.next(), not at the moment the collection mutates. The mutation succeeds; the iterator notices on the next read. That delay is why the stack trace points at the loop, not at the line that called remove() or add(). Knowing this saves you from staring at a for statement that looks innocent; the real culprit is somewhere below it inside the loop body or, in concurrent cases, on another thread entirely.
The exception is also not actually about concurrency in the threading sense. It is a fail-fast detector that originally targeted single-threaded mutation-during-iteration bugs and got the unfortunate name ConcurrentModificationException because the underlying mechanism (modification count comparison) is the same one used to detect multi-thread races. JDK 8 partially fixed the naming confusion by adding removeIf() and proper concurrent collections, but the exception itself still surfaces in both single-threaded and multi-threaded scenarios with the same stack trace.
In Production: Incident Lens
ConcurrentModificationException in production is one of two stories: a single-threaded logic bug that escaped lower environments, or a real multi-threaded race that only manifests under traffic. Telling them apart in the first five minutes shapes the entire response.
- How it surfaces: A 5xx spike on a specific endpoint, with a stack trace pointing at
ArrayList$Itr.checkForComodificationorHashMap$HashIterator.nextNode. The pattern is usually intermittent; it works for 99% of requests, fails for the ones whose data triggers a particular iteration path. In a Spring Boot app, the exception bubbles up to the default error handler and returns 500. APM tools (New Relic, Datadog APM) tag the transaction with the exception class. - Blast radius: Per-request if the mutated collection is request-scoped (local variable inside a handler), which is the easy case (just that one request fails). Tenant-wide or global if the collection is shared state: a
@Componentfield, a static cache, a Spring singleton service holding mutable state. The latter is the dangerous case: any thread iterating sees the corruption from any thread writing, and the exception is the lucky outcome; silent corruption is worse. - What catches it: APM error tracking, sentry-style exception aggregation, and 5xx-rate SLO burn alerts. Thread dumps (
jstack,jcmd <pid> Thread.print) confirm whether multiple threads are touching the same collection; look for two threads with stack frames pointing at the same field. If you only see one thread, it is the single-threaded “modify during iteration” bug. - Recovery sequence: If the bug is single-threaded and surfaces on specific input shapes, you can sometimes mitigate by rejecting the triggering input at the load balancer (WAF rule, rate limit on that endpoint) while you ship a forward-fix. If the bug is multi-threaded on shared state, rollback is usually safer than forward-fix because the wrong fix can make the race worse. The rollback signal is “the previous version did not have this stack trace”; confirm with the deploy history before rolling back.
- Postmortem preventive: For request-scoped mutations, add an integration test that exercises the exact iteration path with a removal predicate. For shared state, the durable control is switching the collection type to a concurrent equivalent (
CopyOnWriteArrayListfor read-heavy lists,ConcurrentHashMapfor maps) and adding a static-analysis rule (SpotBugsFieldShouldBeFinal, ArchUnit rules) that flags non-thread-safe collections used as Spring singleton fields.
When to Use Which Fix
The next nine sections cover the fixes in detail. The table below maps your situation to the recommended fix.
| Your situation | Recommended fix | Why |
|---|---|---|
| Removing elements by predicate, single-threaded | Fix 3: removeIf(predicate) | Cleanest one-liner; Java 8+ |
| Removing by complex logic in a loop | Fix 1: Iterator.remove() in manual while | Updates mod count internally |
| Need to add or replace during iteration | Fix 2: ListIterator.add() / .set() | Only ListIterator supports these |
| Want to collect new collection without touching source | Fix 5: stream().filter().collect() | Side-effect-free transformation |
| Cannot use functional APIs (legacy codebase) | Fix 4: collect to side list, apply after loop | Always safe pattern |
| Single thread, read-heavy collection, occasional writes | Fix 6: CopyOnWriteArrayList | Snapshot iterators never throw |
| Multiple threads writing a map | Fix 7: ConcurrentHashMap | Designed for concurrent access |
| Quick patch where pattern is unclear | Fix 8: iterate over new ArrayList<>(original) | Snapshot approach |
| Confirmed multi-threaded race on shared collection | Fix 9: synchronizedList + manual sync, or concurrent type | Iteration needs explicit lock |
If multiple rows apply, pick the topmost one.
Fix 1: Use Iterator.remove()
The iterator’s own remove() method safely removes the current element without breaking iteration:
Broken:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
for (String name : names) {
if (name.startsWith("B")) {
names.remove(name); // ConcurrentModificationException!
}
}Fixed:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.startsWith("B")) {
it.remove(); // Safe, removes via the iterator
}
}Iterator.remove() updates the modification count internally, so the fail-fast check does not trigger.
Note: Iterator.remove() only removes the last element returned by next(). You cannot call it twice in a row without calling next() in between.
My own rule: Iterator.remove() is only for removal. The moment I find myself wanting to add elements during iteration, I switch to ListIterator (Fix 2 below) or collect the additions in a side list and merge them after the loop ends. Mixing the two intents inside one iteration block is the kind of code I refactor on sight in code review.
Fix 2: Use ListIterator for Add and Set
ListIterator extends Iterator with add() and set() methods:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
ListIterator<String> it = names.listIterator();
while (it.hasNext()) {
String name = it.next();
if (name.equals("Bob")) {
it.set("Robert"); // Replace current element
it.add("Bobby"); // Insert after current element
}
}
// [Alice, Robert, Bobby, Charlie]ListIterator works only with List implementations, not with Set or Map.
Fix 3: Use removeIf() (Java 8+)
The cleanest way to remove elements by condition:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.removeIf(name -> name.startsWith("B"));
// [Alice, Charlie]This works on any Collection (lists, sets, queues). It handles the iterator management internally.
For maps, use entrySet().removeIf():
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 45);
scores.put("Charlie", 70);
scores.entrySet().removeIf(entry -> entry.getValue() < 50);
// {Alice=90, Charlie=70}Fix 4: Collect and Modify After Iteration
Iterate first, collect modifications, then apply them:
For removals:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
List<String> toRemove = new ArrayList<>();
for (String name : names) {
if (name.startsWith("B")) {
toRemove.add(name);
}
}
names.removeAll(toRemove);For additions:
List<String> names = new ArrayList<>(List.of("Alice", "Bob"));
List<String> toAdd = new ArrayList<>();
for (String name : names) {
if (name.equals("Alice")) {
toAdd.add("Alice Jr.");
}
}
names.addAll(toAdd);This pattern is always safe because the modification happens after iteration completes.
Fix 5: Use Streams to Filter
Create a new collection from the filtered stream:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
List<String> filtered = names.stream()
.filter(name -> !name.startsWith("B"))
.collect(Collectors.toList());
// [Alice, Charlie]Warning: Do not modify the source collection inside a stream operation:
// WRONG: throws ConcurrentModificationException:
names.stream().forEach(name -> {
if (name.startsWith("B")) {
names.remove(name); // Modifying source during stream!
}
});Streams should be side-effect-free. Use filter() and collect() instead of modifying the source.
A bug I have caught in PRs more times than any other Java mistake: calling stream().forEach() and mutating the collection the stream was built from. Streams are for transformation; they produce new collections, not mutations of the source. When I see “mutate during forEach” in a diff, I push back with either removeIf(predicate) (which is fail-fast-safe by design) or Iterator.remove() inside a classic loop.
Fix 6: Use CopyOnWriteArrayList
For concurrent access from multiple threads, use CopyOnWriteArrayList:
import java.util.concurrent.CopyOnWriteArrayList;
List<String> names = new CopyOnWriteArrayList<>(List.of("Alice", "Bob", "Charlie"));
// Safe even with multiple threads
for (String name : names) {
if (name.startsWith("B")) {
names.remove(name); // No exception!
}
}CopyOnWriteArrayList creates a new copy of the array on every write operation. Iterators work on a snapshot and never throw ConcurrentModificationException.
Trade-off: Write operations (add, remove, set) are expensive because they copy the entire array. Use this only when reads vastly outnumber writes.
Fix 7: Use ConcurrentHashMap
For thread-safe map operations:
import java.util.concurrent.ConcurrentHashMap;
Map<String, Integer> scores = new ConcurrentHashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 45);
scores.put("Charlie", 70);
// Safe iteration with modification:
scores.forEach((name, score) -> {
if (score < 50) {
scores.remove(name);
}
});ConcurrentHashMap allows concurrent read and write operations without throwing ConcurrentModificationException. Its iterators are weakly consistent: they reflect some but not necessarily all modifications made after the iterator was created.
For HashMap vs ConcurrentHashMap:
| Feature | HashMap | ConcurrentHashMap |
|---|---|---|
| Thread-safe | No | Yes |
| Null keys/values | Yes | No |
| Fail-fast iteration | Yes | No (weakly consistent) |
| Performance (single-thread) | Faster | Slightly slower |
If you need thread-safe operations and null values, use Collections.synchronizedMap() with explicit synchronization during iteration.
Fix 8: Iterate Over a Copy
If you cannot use the above approaches, iterate over a copy of the collection:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
// Iterate over a copy, modify the original:
for (String name : new ArrayList<>(names)) {
if (name.startsWith("B")) {
names.remove(name);
}
}new ArrayList<>(names) creates a shallow copy. The loop iterates over the copy while modifications happen on the original.
For maps:
Map<String, Integer> scores = new HashMap<>();
// ... populate ...
for (Map.Entry<String, Integer> entry : new HashMap<>(scores).entrySet()) {
if (entry.getValue() < 50) {
scores.remove(entry.getKey());
}
}This works but creates extra objects. Prefer removeIf() or Iterator.remove() when possible.
Fix 9: Fix Multi-Threaded Access
If the error occurs across threads, synchronize access to the collection:
List<String> names = Collections.synchronizedList(new ArrayList<>());
// All single operations are thread-safe:
names.add("Alice");
names.remove("Bob");
// BUT iteration must be manually synchronized:
synchronized (names) {
for (String name : names) {
System.out.println(name);
}
}Collections.synchronizedList() synchronizes individual operations but not iteration. You must wrap the entire iteration in a synchronized block.
For better performance, use CopyOnWriteArrayList (read-heavy) or ConcurrentLinkedQueue (write-heavy).
If multi-threading causes OutOfMemoryError from too many threads, see Fix: Java OutOfMemoryError.
The Subtle Causes I Have Hit in the Wild
If you have checked all the fixes above and the exception still fires, these are the less obvious causes I have personally hunted down:
Check for indirect modifications. A method called inside the loop might modify the collection without you realizing it. Trace the full call chain.
Check for subList modifications. List.subList() returns a view of the original list. Modifying the original list invalidates the sublist:
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
List<String> sub = names.subList(0, 2); // [Alice, Bob]
names.add("Dave"); // Invalidates sub
sub.get(0); // ConcurrentModificationException!Check for singleton or empty collections. Collections.singletonList() and Collections.emptyList() return immutable collections. Modifying them throws UnsupportedOperationException, not ConcurrentModificationException. If you see a different exception, see Fix: Java ClassNotFoundException for classpath issues that might cause unexpected collection types.
Check for Kotlin interop. If Java code receives a Kotlin collection, it might be immutable. Wrap it in a mutable copy: new ArrayList<>(kotlinList).
Use the debugger. Set a breakpoint on ConcurrentModificationException (in IntelliJ: Run → View Breakpoints → Add Exception Breakpoint). This stops execution at the exact point the modification count mismatch is detected.
Check for Spring @Cacheable returning shared collections. Spring’s cache abstraction returns the same collection instance to every caller by default. If one request mutates the cached list (e.g., calls .sort() or filters in place), every other concurrent request iterating it crashes. Always defensively copy collections returned from @Cacheable methods, or configure the cache to wrap with Collections.unmodifiableList().
Check for Jackson deserialization into shared instances. If Jackson is configured with a shared ObjectMapper and you use @JsonDeserialize with a custom deserializer that mutates a class-level field, two requests can collide. Keep deserializers stateless.
Check for Hibernate lazy collections. Iterating a @OneToMany lazy collection while another thread loads it (or while the session is closing) can produce ConcurrentModificationException instead of the more common LazyInitializationException. Always fetch the collection inside the transaction with Hibernate.initialize() or a JOIN FETCH query before exposing it to other threads.
Check for Kotlin coroutine context. If you launch coroutines on a shared CoroutineScope and pass a mutable list between them, you have shared mutable state with no thread safety. Use Mutex, StateFlow, or wrap with Collections.synchronizedList() plus an external synchronized iteration block.
For Java applications that crash repeatedly under load, also see Fix: Java NullPointerException; race conditions on shared maps can produce NPE when a thread reads a key that another thread just removed.
What Other Tutorials Get Wrong About This Exception
Most Java tutorials list the same fixes but frame them in ways that produce subtle bugs.
They recommend synchronized blocks as a default. Adding synchronized does NOT fix single-threaded mutation-during-iteration; the modification count check inside Iterator.next() still triggers because the same thread mutated the collection. Tutorials that recommend synchronization as the first fix send readers chasing a non-existent threading bug.
They omit removeIf() despite it being the cleanest fix. Java 8 added Collection.removeIf(predicate) which is fail-fast-safe by design, works on any Collection, and reads naturally. Articles written before Java 8 or copy-pasted from older sources still show the Iterator.remove() while-loop pattern when a one-liner exists.
They confuse Iterator.remove() with Collection.remove(). Calling list.remove(item) inside a for-each loop is the bug; calling iterator.remove() inside a while(it.hasNext()) loop is the fix. Tutorials that say “use remove()” without naming which remove() produce code that still throws.
They miss the stream-mutation prohibition. stream().forEach(item -> source.remove(item)) is the second-most-common cause of this exception, after for-each with list.remove(). Tutorials that present forEach as a drop-in replacement for for loops miss that streams must be side-effect-free.
They treat CopyOnWriteArrayList as a universal multi-threading fix. It is excellent for read-heavy workloads but pathological for write-heavy ones; every write copies the entire array. Articles that recommend it without warning produce O(n) per-write code that grinds production to a halt.
They confuse Collections.synchronizedList() with thread-safe iteration. synchronizedList() synchronizes individual operations but does NOT synchronize iteration. The for-each loop must be wrapped in a synchronized (list) { ... } block manually. Tutorials that show Collections.synchronizedList() without the wrap leave readers with broken concurrent code.
Frequently Asked Questions
Does this exception only happen with multiple threads?
No. The name is misleading. It fires whenever a collection’s modification count changes between iteration steps, including when the same thread mutates the collection inside the loop body. Most occurrences in production are single-threaded code calling list.remove(item) inside a for-each loop.
What is the difference between Iterator.remove() and Collection.remove()?
Iterator.remove() updates the iterator’s internal modification count along with the collection’s count, so the fail-fast check stays consistent. Collection.remove() only updates the collection’s count, leaving the iterator with a stale expected-count value; the next iterator.next() detects the mismatch and throws.
Why does my stream().forEach() throw this exception?
Because the lambda mutates the same collection that the stream was built from. Streams are designed for side-effect-free transformation: read from a source, produce a new result. Mutating the source from inside forEach violates the contract and triggers the fail-fast detector. Use filter().collect() to produce a new collection instead.
Is removeIf() thread-safe?
No (on ArrayList, HashMap, etc.). It is ConcurrentModificationException-safe within a single thread because it manages the iterator internally. For multi-threaded use, switch to a concurrent collection: CopyOnWriteArrayList.removeIf() is thread-safe; ConcurrentHashMap.entrySet().removeIf() is thread-safe.
When should I use CopyOnWriteArrayList vs Collections.synchronizedList()?
CopyOnWriteArrayList for read-heavy workloads where writes are infrequent (5-10% of operations). Reads are lock-free and iterators never throw. Collections.synchronizedList() for balanced workloads where you can wrap iteration in a synchronized block. Never use CopyOnWriteArrayList for write-heavy code; every write copies the entire backing array.
Why does HashMap throw this even on a single thread?
Same mechanism: the iterator tracks a modification count, and mutating the map (put, remove) while iterating triggers the check. The fix is entrySet().removeIf(predicate) for conditional removal or ConcurrentHashMap for genuine concurrent access.
For similar issues in Python where modifying a list during iteration causes problems, see Fix: Python IndexError: list index out of range.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Java Record Not Working — Compact Constructor Error, Serialization Fails, or Cannot Extend
How to fix Java record issues — compact constructor validation, custom accessor methods, Jackson serialization, inheritance restrictions, and when to use records vs regular classes.
Fix: OpenTelemetry Not Working — Traces Not Appearing, Spans Missing, or Exporter Connection Refused
How to fix OpenTelemetry issues — SDK initialization order, auto-instrumentation setup, OTLP exporter configuration, context propagation, and missing spans in Node.js, Python, and Java.
Fix: Spring Boot Test Not Working — ApplicationContext Fails to Load, MockMvc Returns 404, or @MockBean Not Injected
How to fix Spring Boot test issues — @SpringBootTest vs test slices, MockMvc setup, @MockBean vs @Mock, test context caching, and common test configuration mistakes.
Fix: Spring Boot @Cacheable Not Working — Cache Miss Every Time or Stale Data
How to fix Spring Boot @Cacheable issues — @EnableCaching missing, self-invocation bypass, key generation, TTL configuration, cache eviction, and Caffeine vs Redis setup.