Skip to content

Fix: Java ConcurrentModificationException

FixDevs · (Updated: )

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 Error

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.

Why This Happens

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 ArrayList or HashMap.
  • 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.checkForComodification or HashMap$HashIterator.nextNode. The pattern is usually intermittent — 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 @Component field, 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 — CopyOnWriteArrayList for read-heavy lists, ConcurrentHashMap for maps — and adding a static-analysis rule (SpotBugs FieldShouldBeFinal, ArchUnit rules) that flags non-thread-safe collections used as Spring singleton fields.

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.

Pro Tip: Use Iterator.remove() only when you need to remove elements. If you need to add elements during iteration, use ListIterator (Fix 2) or collect additions in a separate list and add them after the loop.

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.

Common Mistake: Using stream().forEach() to modify the collection the stream is based on. Streams are designed for transformation, not mutation. If you need to modify in place, use removeIf() or Iterator.remove().

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:

FeatureHashMapConcurrentHashMap
Thread-safeNoYes
Null keys/valuesYesNo
Fail-fast iterationYesNo (weakly consistent)
Performance (single-thread)FasterSlightly 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.

Still Not Working?

If you have checked all the fixes above:

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.

For similar issues in Python where modifying a list during iteration causes problems, see Fix: Python IndexError: list index out of range.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles