Ch. 7 · Java

Java ConcurrentModificationException: Removing Items Safely

Fix java.util.ConcurrentModificationException when removing items in a loop: use removeIf or Iterator.remove, and see when concurrent lists help.

~7 min readintermediateupdated Oct 4, 2026

You were looping over a list or map, removed or added something, and got this:

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 Cleanup.main(Cleanup.java:9)
Text

There is no message after the class name; that is normal. The exception means an iterator noticed that its collection was structurally changed (an element added or removed) by something other than the iterator itself while iteration was in progress. Despite the name, there usually is no second thread involved: the most common cause is a single thread calling list.remove(...) inside a for-each loop over that same list. The line numbers inside ArrayList vary between JDK releases; the frame that matters is the one in your code.

Quick fix checklist

  • Find the loop at the line in your own code: it is iterating a collection that the loop body (or a method it calls) modifies.
  • To delete matching elements, replace the loop with collection.removeIf(predicate).
  • If you need the loop body for other work, use an explicit Iterator and call it.remove().
  • For maps, use map.entrySet().removeIf(...) or map.values().removeIf(...).
  • To add elements, collect them into a separate list and call addAll after the loop, or build a new collection.
  • Only reach for CopyOnWriteArrayList or ConcurrentHashMap when several threads genuinely share the collection.

Before you start

You should know how the enhanced for loop works: for (String s : list) is compiled to Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); ... }. Once you see the hidden iterator, the exception makes sense. The examples use ArrayList and HashMap on Java 17 or later, but the rules apply to every collection in java.util that is not designed for concurrency (LinkedList, HashSet, TreeMap and so on).

Why it happens

ArrayList, HashMap and their siblings keep a counter called modCount. Every structural change (add, remove, clear, resizing a map) increments it. When you create an iterator, it records the current value as expectedModCount. Each call to next() compares the two. If they differ, the iterator knows its view of the collection is stale: indexes may have shifted, a bucket may have been rehashed, and continuing could skip or repeat elements. Rather than return wrong results, it throws immediately. This is called fail-fast behaviour.

The iterator’s own remove() method updates both counters, so it is the one sanctioned way to delete during iteration. Replacing the value for an existing key with map.put or calling list.set(i, x) is not a structural change and does not trigger the check.

Two consequences surprise people. First, the check happens in next(), so if the modification makes hasNext() return false, the loop just ends quietly and you get no exception, only wrong results. Second, the Javadoc describes fail-fast behaviour as best effort: with real concurrent access the check can miss a modification, so a missing exception never proves your code is thread-safe.

Step-by-step walkthrough

Step 1: Reproduce it in one thread

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

public class Cleanup {
    public static void main(String[] args) {
        List<String> users = new ArrayList<>(List.of("ann", "bob", "cat", "dan"));

        for (String user : users) {
            if (user.startsWith("a")) {
                users.remove(user);
            }
        }
        System.out.println(users);
    }
}
java

After ann is removed, the list has three elements and modCount has moved on. The loop’s hidden iterator calls next() for the second element, sees the mismatch and throws.

Step 2: Notice the silent variant

Change the condition to remove cat instead, which is the second-to-last element:

if (user.equals("cat")) {
    users.remove(user);
}
java

No exception. After the removal the list has three elements, the iterator’s cursor is already at 3, hasNext() returns false and the loop exits without ever visiting dan. Code like this passes a quick test and fails later when the data changes. Treat “it didn’t throw” as no evidence at all.

Step 3: Remove with removeIf or Iterator.remove

When the only job is deletion, removeIf is the clearest and, for ArrayList, the fastest option: it compacts the array in one pass instead of shifting elements for every removal.

users.removeIf(user -> user.startsWith("a"));
java

When the loop does other work as well, use the iterator explicitly:

import java.util.Iterator;

Iterator<String> it = users.iterator();
while (it.hasNext()) {
    String user = it.next();
    if (user.startsWith("a")) {
        audit("removing " + user);
        it.remove();
    }
}
java

it.remove() removes the element most recently returned by next(). Calling it twice in a row, or before next(), throws IllegalStateException.

Step 4: Handle additions and maps

You cannot add through a plain Iterator. Collect first, then apply:

List<String> extra = new ArrayList<>();
for (String user : users) {
    if (user.equals("bob")) {
        extra.add("bob-admin");
    }
}
users.addAll(extra);
java

Maps behave the same way. This throws from HashMap$HashIterator.nextNode because put with a new key is structural:

Map<String, Integer> scores = new HashMap<>(Map.of("ann", 0, "bob", 3));
for (String name : scores.keySet()) {
    scores.put(name + "-copy", 0);            // new key: ConcurrentModificationException
}
java

To delete entries, use the views:

scores.entrySet().removeIf(e -> e.getValue() == 0);
java

To transform, build a new map, or use replaceAll when only values change:

scores.replaceAll((name, score) -> score + 1);
java

The same rule applies inside lambdas: modifying a list from within list.forEach(...), or modifying the source of a stream while a terminal operation is running, throws ConcurrentModificationException too.

Step 5: Decide whether threads are really involved

If the stack trace shows a loop in one thread and the modification in another (for example a scheduler thread pruning a list that a request thread is iterating), the bug is unsynchronised sharing. Fixing it with removeIf will not help. That is the case where CopyOnWriteArrayList (many reads, rare writes), ConcurrentHashMap or explicit locking is the right tool.

Worked scenario

A notification service keeps a list of subscribers and drops the ones whose sessions have expired while sending:

class Notifier {
    private final List<Subscriber> subscribers = new ArrayList<>();

    void broadcast(String message) {
        for (Subscriber s : subscribers) {
            if (s.isExpired()) {
                unsubscribe(s);
            } else {
                s.send(message);
            }
        }
    }

    void unsubscribe(Subscriber s) {
        subscribers.remove(s);
    }
}
java

In production it threw ConcurrentModificationException a few times a day. The trace pointed at the for-each line. The diagnosis: unsubscribe modifies the list that broadcast is iterating, in the same thread. It only failed when an expired subscriber was not second-to-last, which made it look random.

The fix separates the two concerns: prune first, then send.

void broadcast(String message) {
    subscribers.removeIf(Subscriber::isExpired);
    for (Subscriber s : subscribers) {
        s.send(message);
    }
}
java

Later, subscriptions started arriving from a WebSocket thread while broadcasts ran on a scheduler. Now there are two threads, and the right change is the collection itself:

private final List<Subscriber> subscribers = new CopyOnWriteArrayList<>();
java

Its iterators work on a snapshot of the array, so broadcast never throws and never sees a half-updated list. Every write copies the whole array, which is fine for a subscriber list that changes rarely and is read constantly. Note that CopyOnWriteArrayList iterators do not support it.remove() (it throws UnsupportedOperationException); removeIf works.

Common mistake

Switching to CopyOnWriteArrayList to fix a single-threaded loop makes the exception go away but not the confusion. The loop now iterates a snapshot, so it still visits elements you already removed, and every removal copies the entire array, turning an O(n) cleanup into O(n squared). It also tells the next reader the collection is shared between threads when it is not.

Other tempting fixes that go wrong:

  • Looping by index and calling list.remove(i): it avoids the iterator, but every removal shifts later elements left, so the next element is skipped unless you decrement i or loop backwards.
  • Catching ConcurrentModificationException and carrying on: the iteration state is already wrong.
  • Wrapping with Collections.synchronizedList: individual calls are locked, but iteration still has to be done inside synchronized (list) { ... }, and it does not help when the same thread modifies the list.

Verify the behavior

Pin both the broken pattern and the fix in tests so a refactor cannot bring the loop back:

import static org.junit.jupiter.api.Assertions.*;

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
import org.junit.jupiter.api.Test;

class CleanupTest {
    @Test
    void removingInsideForEachFailsFast() {
        List<String> users = new ArrayList<>(List.of("ann", "bob", "cat"));
        assertThrows(ConcurrentModificationException.class, () -> {
            for (String u : users) {
                if (u.startsWith("a")) users.remove(u);
            }
        });
    }

    @Test
    void removeIfDeletesEveryMatch() {
        List<String> users = new ArrayList<>(List.of("ann", "amy", "bob", "abe"));
        assertDoesNotThrow(() -> users.removeIf(u -> u.startsWith("a")));
        assertEquals(List.of("bob"), users);
    }
}
java

Use test data with several adjacent matches, including the last element, so the silent-skip variant would fail the assertion.

Interview exercise

An interviewer shows a for-each loop that removes elements from an ArrayList and says: “This passed our test, but it throws in production. Why, and what is the cleanest fix?”

Answer and reasoning

The enhanced for loop uses the list’s iterator, which checks modCount on every next() call. Removing through list.remove changes modCount behind the iterator’s back. The test probably removed the second-to-last element: after that removal hasNext() returns false, so next() never runs, no check happens, and the last element is silently skipped. Production data hit a different position and threw. The cleanest fix is list.removeIf(predicate), which is correct and linear time; if the loop must do other work, use an explicit iterator and it.remove(). I would also mention that ConcurrentModificationException does not imply multiple threads, and that concurrent collections are the answer only when the list is actually shared.

Continue learning

More in Java

esc