<code>ConcurrentModificationException</code> in Java
ConcurrentModificationException is thrown when a collection's structure changes while it's being iterated. Surprisingly, it's not about threads β it's thrown even from single-threaded code that modifies a collection inside its own for loop.
The classic trigger
List<String> xs = new ArrayList<>(List.of("a", "b", "c"));
for (String s : xs) {
if (s.equals("b")) xs.remove(s); // β CME
}
The enhanced for uses an Iterator behind the scenes. Iterators track a modification count and throw CME when they detect a change that didn't go through the iterator.
The three safe patterns
// 1. Iterator.remove()
var it = xs.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove();
}
// 2. removeIf (Java 8+) β cleanest
xs.removeIf(s -> s.equals("b"));
// 3. Collect what to remove, then removeAll
var toRemove = xs.stream().filter(s -> s.equals("b")).toList();
xs.removeAll(toRemove);
Adding during iteration
You can't add either. Build a separate list and combine:
var toAdd = new ArrayList<String>();
for (String s : xs) {
if (s.length() == 1) toAdd.add(s + "!");
}
xs.addAll(toAdd);
Multi-threaded reads + writes
For true concurrent access across threads, use a concurrent collection β CopyOnWriteArrayList (read-heavy), ConcurrentHashMap (general-purpose map). They don't throw CME but have their own semantics (e.g. the iterator is a snapshot).
Maps β use entrySet iterator
Map<String,Integer> votes = ...;
for (var it = votes.entrySet().iterator(); it.hasNext(); ) {
var e = it.next();
if (e.getValue() == 0) it.remove();
}
Common mistakes
- Assuming CME is a threading issue β it mostly isn't. Single-threaded add/remove during iteration triggers it.
- Using a
for (int i ...)loop to "avoid" CME β you'll skip elements whenremoveshifts indexes. - Synchronising a non-concurrent collection for writes but reading without the lock β race conditions and possibly CME.
Related
Pillar: Java exceptions. See also Iterator and removeIf.