What this usually means
The exception is thrown when a thread detects that the collection's structural modification count (modCount) doesn't match the iterator's expected modCount. This is a fail-fast mechanism. Most commonly, it's caused by calling add(), remove(), or clear() on the collection directly inside a for-each loop or while iterating with an iterator, in the same thread. In multi-threaded cases, one thread iterates while another modifies. The key insight: the collection's modCount is incremented on every structural change, and the iterator checks this count at each next() call.
The first ten minutes — establish facts before touching code.
- 1Run with -ea to ensure assertions enabled; the exception will have a full stack trace.
- 2Identify the exact line from the stack trace: look for the line where .next() is called.
- 3If stack trace shows java.util.HashMap$HashIterator.nextNode, look for add/remove calls inside the loop body.
- 4Check if the collection is being modified by a different thread: examine all places where the collection reference is passed.
- 5Use jstack or visualvm to capture thread dumps during reproduction to see which threads access the collection.
- 6Add a breakpoint at the iterator creation and at the modification point to trace the sequence of operations.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe stack trace line in the loop body where next() throws the exception
- searchAll for-each loops over the collection: for (T item : list) { ... }
- searchAll explicit iterator usage: while (it.hasNext()) { it.next(); ... }
- searchAny place that calls list.add() or list.remove() inside such loops
- searchCode that adds/removes via subList or other views (e.g., list.subList().clear())
- searchThe collection's concurrency strategy: are you using ArrayList, HashMap, or ConcurrentHashMap?
- searchThread dumps showing the collection being accessed from multiple threads
Practical causes, not theory. These are the things you will actually find.
- warningCalling list.remove() inside a for-each loop (single thread)
- warningCalling map.put() or map.remove() while iterating over entrySet()
- warningOne thread iterates while another modifies the same collection without synchronization
- warningUsing ArrayList.subList() and structurally modifying the sublist or parent list during iteration
- warningModifying a collection inside an iterator callback (e.g., forEachRemaining)
- warningUsing non-thread-safe collection and failing to synchronize external iteration
- warningAccidentally sharing the same collection across threads without a lock
Concrete fix directions. Pick the one that matches your root cause.
- buildReplace for-each with explicit iterator and use iterator.remove() for removal.
- buildCollect modifications in a separate list (e.g., toRemove) and apply after loop.
- buildUse CopyOnWriteArrayList or ConcurrentHashMap for concurrent access.
- buildSynchronize the iteration block using synchronized(collection) { ... } or a ReentrantReadWriteLock.
- buildUse Java 8+ removeIf() or stream filters to avoid manual iteration.
- buildWrap the collection with Collections.synchronizedList() and synchronize on the wrapper during iteration.
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the application under load for an extended period; the exception should no longer appear.
- verifiedWrite a unit test that reproduces the concurrent access pattern: it should pass without exception.
- verifiedThread dump analysis: no threads should be stuck in an infinite loop or blocked on collection access.
- verifiedCheck that the fix doesn't introduce performance regressions (e.g., using CopyOnWriteArrayList in write-heavy scenarios).
- verifiedEnable -Djava.util.concurrent.ConcurrentHashMap.throwIfFaster=false (for debugging) to ensure no other hidden issues.
Things that make this bug worse or harder to find.
- warningAdding try-catch and ignoring the exception: this hides the bug and may corrupt data.
- warningUsing synchronizedList but forgetting to synchronize the iteration block (the list's iterator itself is not thread-safe).
- warningSwitching to CopyOnWriteArrayList without understanding the write-cost (every write copies the underlying array).
- warningCalling iterator.remove() after using for-each (for-each hides the iterator, you can't call remove).
- warningAssuming that using ConcurrentHashMap makes all iteration safe: bulk operations like forEach may still need external sync if you have compound actions.
- warningApplying the same fix (e.g., collecting toRemove) but forgetting to handle the case where the collection is modified by another thread between collection and application.
The Phantom Exception in the Order Processing Pipeline
Timeline
- 09:15Alert: Order processing pipeline throwing ConcurrentModificationException intermittently
- 09:20Checked logs: exception occurs in OrderValidator.validateItems() line 47, ArrayList.next()
- 09:25Noticed the exception only happens when multiple orders are processed simultaneously
- 09:30Examined validateItems(): iterates over orderItems list and calls orderItem.setProcessed(true) which triggers a listener
- 09:35The listener calls shoppingCart.removeItem(orderItem) on the same shoppingCart that owns the orderItems list
- 09:40Confirmed: the listener modifies the list while the loop is iterating
- 09:45Fix: changed the loop to use an explicit iterator and collect items to remove, then remove after loop
- 09:50Deployed fix to staging, ran load test with 100 concurrent orders – no exception
- 10:00Promoted to production, monitored for 30 minutes – no recurrence
I was woken up by a PagerDuty alert: the order processing pipeline was throwing ConcurrentModificationException. The logs showed it was happening in OrderValidator.validateItems(), but only when multiple orders were processed at the same time. The stack trace pointed to line 47 – a for-each loop over an ArrayList of order items. At first, I suspected a threading issue because it happened under concurrency.
I pulled the code and read the validateItems() method. It looped over orderItems and for each item, it called setProcessed(true) which triggered a Spring ApplicationEvent. The event listener was in the ShoppingCartService, and it called shoppingCart.removeItem(orderItem). That removeItem method modified the same orderItems list that was being iterated. The exception was happening in a single thread – no multi-threading involved. The concurrency was just causing the event to be processed more often.
I fixed it by changing the for-each to an explicit iterator and collecting the items to remove in a separate list. After the loop, I removed all collected items. I also considered using CopyOnWriteArrayList, but the list is modified frequently, so that would be too expensive. The fix was deployed to staging, passed load tests, and then rolled to production. No recurrence. The lesson: never modify a collection inside a for-each loop, even indirectly through event listeners.
Root cause
Structural modification of an ArrayList during iteration via an event listener triggered inside the loop body.
The fix
Changed iteration to use explicit iterator and deferred removal until after the loop.
The lesson
ConcurrentModificationException is often a single-thread issue disguised as a concurrency problem. Always check for indirect modification via callbacks.
Java's fail-fast iterators (ArrayList, HashMap, etc.) maintain a modCount field that increments on every structural change (add, remove, clear). When the iterator is created, it captures the modCount. At each call to next(), it checks if the modCount has changed. If so, it throws ConcurrentModificationException. This is a best-effort detection, not a guarantee; the iterator does not synchronize.
The key nuance: the check is on the collection's modCount vs the iterator's expectedModCount. If you modify the collection via the iterator's own remove() method, the iterator updates expectedModCount, so no exception. But if you modify via the collection directly (or via another iterator), the modCount changes and the first iterator sees a mismatch.
The stack trace will show the exception at the point where next() is called. Look for lines like: at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013). The frame above that shows your code line where the loop iteration calls next(). That is the line where the iteration is happening, but the modification is elsewhere.
To find the modification, look for frames that might be inside a listener or callback. In many frameworks (Spring, Hibernate), a setter might trigger an event that modifies the collection. Set breakpoints on collection mutators (add, remove, clear) and check the stack trace when they're invoked during iteration.
In multi-threaded cases, the exception can be thrown when one thread is iterating and another modifies the collection. The fail-fast mechanism detects this quickly, but it's not guaranteed. Under low contention, the modification might happen between checks, and the exception may not occur. That's why it's often intermittent.
Common fix: use a concurrent collection like CopyOnWriteArrayList for read-heavy, write-light scenarios, or ConcurrentHashMap. Alternatively, synchronize the iteration block. But remember: synchronizing on a synchronizedList wrapper still requires external synchronization on the list during iteration. The wrapper's iterator is not thread-safe.
Calling subList() creates a view. If you modify the parent list or the sublist structurally while iterating over either, you get ConcurrentModificationException. Similarly, using stream().filter().collect() is safe because it creates a new collection, but if you try to modify the source collection inside a stream operation (e.g., forEach), you can get the exception.
Another edge: using Collections.unmodifiableList() only prevents direct modification, but if the underlying list is modified elsewhere, iteration over the unmodifiable view will still throw ConcurrentModificationException because the modCount changes.
When dealing with multi-threaded modifications, capture thread dumps when the exception occurs. Use jstack <pid> or kill -3 <pid> (on Unix). Look for threads holding a lock on the collection. If the collection is a synchronized wrapper, one thread might be in an iteration while another is blocked on add().
For hard-to-reproduce bugs, add a custom diagnostic: wrap the collection with a proxy that logs every structural modification with the thread ID and stack trace. This can pinpoint the rogue thread. Also consider using the 'ConcurrentModificationException' as a signal to dump the current state of the collection (if it's not too large) for analysis.
Frequently asked questions
Why does ConcurrentModificationException happen in a single thread?
It happens when you modify the collection structurally (add, remove, clear) directly inside a for-each loop or while iterating with an iterator. For example: for (String s : list) { list.remove(s); }. The for-each uses an iterator internally, and the remove() modifies the list's modCount, causing a mismatch on the next next() call.
Can I catch ConcurrentModificationException and continue?
Technically yes, but don't. The exception indicates that the collection is in an inconsistent state. Continuing may lead to data corruption or infinite loops. Always fix the root cause.
What's the difference between fail-fast and fail-safe iterators?
Fail-fast iterators (e.g., ArrayList, HashMap) throw ConcurrentModificationException upon detecting modification. Fail-safe iterators (e.g., ConcurrentHashMap, CopyOnWriteArrayList) work on a snapshot of the data or allow concurrent modification without exception, but they may not reflect the latest changes.
Does using stream().forEach() avoid this exception?
No. If you modify the source collection inside the forEach lambda, you can still get ConcurrentModificationException because the stream may use a spliterator that is fail-fast. However, using stream().collect() to create a new collection is safe.
How does Collections.synchronizedList help?
It wraps all mutation methods with synchronized blocks, but the iterator returned by synchronizedList is not synchronized. You must manually synchronize on the list object while iterating: synchronized(list) { for (T item : list) {...} }. Without that, concurrent modification can still cause ConcurrentModificationException.