🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOGException in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050) at CmeDemo.main(CmeDemo.java:11)

⚡ Quick Fix Works 80% of the time

Remove through the iterator itself, or replace the whole loop with removeIf.

// Safe removal while iterating for (Iterator<String> it = names.iterator(); it.hasNext(); ) { if (it.next().equals("ravi")) { it.remove(); } } // Or one line since Java 8: names.removeIf(name -> name.equals("ravi"));

🧠 Why this Happens

Tap to expand the deep technical explanation

ArrayList inherits a modCount revision counter from AbstractList; every structural change bumps it. When iterator() hands out an Itr, that object snapshots expectedModCount = modCount, and every next() first calls checkForComodification to compare the two values. list.remove() bumps only modCount, so the mismatch fires on the following advance; iterator.remove() bumps BOTH values, which is precisely why it can never trigger the exception. The check lives in next(), not hasNext(): after removing the second-to-last element the size drops, nextIndex() equals size, hasNext() returns false, the enhanced-for exits normally — and the poisoned comparison never executes. Despite the name, threads are entirely optional: any unsynchronized structural change during traversal triggers it.

The HITEC City Parking Spot Analogy:

The iterator is a tally clerk walking a conveyor belt with his own duplicate ledger. Yank an item off the belt behind his back (list.remove()) and his ledger stops matching reality — he halts the line at the next crate rather than certify wrong numbers. Hand items through him instead (it.remove()) and he updates his own ledger as he goes.

🔁 How to Reproduce Confirm this is your error

Create a four-element ArrayList ("raju", "ravi", "kiran", "suresh") and call names.remove(name) inside a for-each when name equals "ravi" — the very next iterator advance throws. Verified lab nuance: with only THREE elements, removing the second-to-last exits silently because hasNext() sees cursor == size after the shrink and never reaches checkForComodification. (Lab capture: OpenJDK 25.)

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Remove via it.remove(), never list.remove()

👉 Use this in single-threaded loops that must delete entries while scanning.

The iterator is the only writer allowed mid-traversal: its own remove() advances past the deleted element AND re-syncs its private expectedModCount with the list-wide modCount, so they stay equal. list.remove() bumps only modCount — the mismatch detonates at the next next(). It also decrements the internal cursor correctly, avoiding skipped-element bugs that index-based removal causes.

for (Iterator<String> it = names.iterator(); it.hasNext(); ) { String name = it.next(); if (name.startsWith("test_")) { it.remove(); } }
Solution 2

removeIf for condition-based bulk deletion

👉 Use this whenever survivors can be expressed as a predicate and no per-item logic is needed during removal.

Collection.removeIf arrived in Java 8 as a default method. ArrayList overrides it with a bitset-based implementation that shifts surviving elements once — faster than repeated single removes and structurally incapable of desynchronizing, because there is no user-facing iterator to go stale.

// Keep everything except disabled accounts accounts.removeIf(a -> !a.isActive()); // Multi-condition orders.removeIf(o -> o.isExpired() || o.value() == 0);
Solution 3

Snapshot first, mutate after

👉 Use this when the walk must see a stable view or when removed and surviving items are needed separately afterwards.

Iterate over a copy so the original can change freely; every mutation happens while NO live iterator points at it. Costs one extra list — trivial against correctness — and streams give the same shape declaratively.

List<String> snapshot = new ArrayList<>(queue); snapshot.forEach(item -> { if (expired(item)) queue.remove(item); // safe: iterating the copy }); // Declarative variant: List<Order> stale = orders.stream().filter(Order::isStale).toList(); orders.removeAll(stale);
Solution 4

Concurrent collections for genuinely parallel mutation

👉 Use this when OTHER threads modify the structure while yours iterates — every fix above assumes single-threaded access.

CopyOnWriteArrayList snapshots its backing array on each write; iterators walk that immutable picture and NEVER throw, trading an O(n) copy per write — ideal for read-mostly listener registries. ConcurrentHashMap iterators are weakly consistent: no exception ever, but they may reflect only part of concurrent updates. Choose by read/write ratio, not habit.

List<Listener> listeners = new CopyOnWriteArrayList<>(); for (Listener l : listeners) l.onEvent(evt); // safe under concurrent add/remove Map<String, Stats> stats = new ConcurrentHashMap<>(); for (var e : stats.entrySet()) { /* weakly consistent view */ }
Solution 5

Backwards index loop for positional compaction

👉 Use this on small random-access lists when positions are already tracked and zero extra allocations matter.

Walking indices from size()-1 down to 0 means every removal shifts only ALREADY-VISITED positions — nothing is skipped and no iterator exists to desynchronize. Avoid on LinkedList, where get(i) is O(n) per step and the combination turns quadratic immediately.

for (int i = users.size() - 1; i >= 0; i--) { if (!users.get(i).hasConsent()) { users.remove(i); } }

📋 Version Notes

Java 8

Collection.removeIf lands as a default method — the one-line bulk fix becomes universally available.

Java 17

Fail-fast semantics unchanged; traces still show ArrayList$Itr.checkForComodification at the throw site (line numbers drift between builds).

Java 21

SequencedCollection adds reverse()/reversed() views but does not relax fail-fast rules for their iterators.

🛡️ How to Prevent This Next Time

Treat enhanced-for bodies as read-only, funnel all removals through removeIf or explicit iterators, and reach for CopyOnWriteArrayList or ConcurrentHashMap only when threads genuinely share the structure.