Course contents

ConcurrentModificationException explained

ConcurrentModificationException in single-threaded loops, the second-to-last removal quirk, safe removal patterns for collections and maps, `HashMap.computeIfAbsent`, `subList` invalidation and multithreaded fixes.

PremiumMust know for interviewsLesson21 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: the iterator has a bookmark
  2. The classic bug: removing inside for-each
  3. The second-to-last removal quirk
  4. Correct ways to remove from Java collections
  5. Removing from maps safely
  6. Less obvious CME sources
  7. Advanced: exact contracts and trade-offs
  8. How to say it in the interview

🚨 What is

ConcurrentModificationException? ConcurrentModificationException (CME) is a runtime exception thrown when an iterator or collection view detects that the underlying collection was structurally changed in a way that invalidates the traversal. The classic example is removing from an ArrayList inside a for-each loop. Despite the name, one thread is enough: "concurrent" means the iterator and the collection are being modified concurrently with each other, not necessarily by different CPU threads.

For this lesson, we'll focus on why CME happens, the famous "remove the second-to-last element and no exception" quirk, the correct removal patterns for lists and maps, and the less obvious sources: HashMap.computeIfAbsent, subList views and real multithreaded races. We covered the iterator families and modCount mechanism in Fail-fast vs fail-safe iterators; here we'll use that mechanism to fix code.

Below the line (out of scope)

The 30-second answer

CME usually means you changed a collection structurally while an iterator or backed view was using it. A for-each loop uses an iterator, so list.remove(x) inside that loop changes modCount behind the iterator's back; the next next() usually sees that modCount != expectedModCount and throws. It can happen in a single thread. The correct fixes are Iterator.remove(), removeIf, ListIterator.set/add when you are editing a list during traversal, iterating over a copy, collecting elements then removing them, streams that build a new collection, or a backwards index loop for lists. For maps, use entrySet().removeIf, an iterator over entrySet, or computeIfPresent returning null. In multithreaded code, CME is a symptom, not a safety feature: use locking, snapshots or concurrent collections.

What you’ll learn

  • Explain why **ConcurrentModificationException can happen in one thread**
  • Trace the second-to-last removal quirk through cursor != size
  • Remove safely with Iterator.remove, removeIf, ListIterator, copies and backwards index loops
  • Use map-specific removal patterns such as entrySet().removeIf and computeIfPresent returning null
  • Recognize CME from HashMap.computeIfAbsent, subList invalidation and shared mutable collections
  • Choose locking or concurrent collections when CME appears in multithreaded code

Interview radar

Asked very often: Why does removing from a list inside for-each throw, and what is the right way to remove? Senior rounds probe: The second-to-last removal quirk, subList invalidation, and computeIfAbsent mapping functions that modify the same map. Production angle: A nightly cleanup loop "sometimes" fails because another code path mutates the same collection view.

01Picture it first: the iterator has a bookmark

Think of an iterator as reading a printed checklist with a bookmark. The list object on the wall has a revision number. If someone edits the wall list while you are reading from your printed checklist, your next check may notice that the revision number changed and stop.

The odd part is that the iterator does not check on every possible method. For ArrayList, hasNext() checks whether the cursor has reached the current size; next() checks for concurrent modification. That split is what creates the second-to-last removal puzzle.

Fig. 1

Removing the second-to-last element from an ArrayList during for-each leaves cursor equal to the new size, so hasNext returns false and next never checks modCount

Removing the second-to-last element from an ArrayList during for-each leaves cursor equal to the new size, so hasNext returns false and next never checks modCount

Notice the lesson from the diagram: CME is detection, not a formal proof. If the check is not reached, the exception is not thrown.

02The classic bug: removing inside for-each

A for-each loop over an Iterable is syntactic sugar for an Iterator. That matters because the iterator owns its expectedModCount, while list.remove(...) modifies the list directly.

Try it yourself first

Before reading on, decide whether this code throws in one thread. Then name the smallest change that makes the removal legal.

List<String> statuses = new ArrayList<>(List.of("NEW", "PAID", "CANCELLED"));

for (String status : statuses) {
    if (status.equals("PAID")) {
        statuses.remove(status);               // structural change outside iterator
    }
}

This usually throws ConcurrentModificationException. The loop's iterator expected the list's original modCount; statuses.remove(status) increments the list's modCount; the next iterator check sees the mismatch.

The smallest legal change is to use the iterator's own remove() method:

List<String> statuses = new ArrayList<>(List.of("NEW", "PAID", "CANCELLED"));

for (Iterator<String> it = statuses.iterator(); it.hasNext(); ) {
    String status = it.next();
    if (status.equals("PAID")) {
        it.remove();                           // iterator performs and records the change
    }
}

System.out.println(statuses);                  // [NEW, CANCELLED]

Notice three things:

  • Iterator.remove() removes the last element returned by next().
  • You may call it once per next() call. Calling it twice in a row throws IllegalStateException.
  • It keeps the iterator's expected modification count aligned with the collection.

This is the fix to say first in an interview because it shows you understand the mechanism. In day-to-day code, removeIf is often clearer.

03The second-to-last removal quirk

Now for the puzzle that separates memorised rules from mechanism. Removing inside for-each does not always throw. In an ArrayList, if you remove the second-to-last element, the iterator can stop without calling next() again, so the modification check never runs.

Try it yourself first

What does each loop print or throw? Try to explain it using cursor and size, not vibes.

List<String> one = new ArrayList<>(List.of("a", "b", "c"));
for (String s : one) {
    if (s.equals("b")) {
        one.remove(s);
    }
}
System.out.println(one);                       // [a, c], no exception

List<String> two = new ArrayList<>(List.of("a", "b", "c"));
for (String s : two) {
    if (s.equals("a")) {
        two.remove(s);
    }
}
System.out.println(two);                       // usually not reached: CME

In the first loop, the iterator has returned b, so its cursor is 2. Removing b shrinks the list size from 3 to 2. The next hasNext() asks cursor != size, which is 2 != 2, so it returns false. next() is not called again, and the modCount check in next() never runs.

In the second loop, after removing a, the cursor is 1 and the new size is 2. hasNext() returns true, then next() checks modCount and throws. This is why I avoid saying "will always throw" in interviews. The better sentence is: it is illegal for the iterator contract and fail-fast detects it on a best-effort basis.

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • Correct ways to remove from Java collections
  • Removing from maps safely
  • Less obvious CME sources
  • Advanced: exact contracts and trade-offs
  • How to say it in the interview

From ₹199 · one-time payment · No auto-renewal

Practise this topicOrdering & iteration: interview questions & scenarios
Further reading (6)