Fail-Fast vs Fail-Safe Iterators in Java: Understanding ConcurrentModificationException
I am pretty sure you have encountered a ConcurrentModificationException when attempting to modify a collection within a loop. Once you encounter it once, you learn the lesson that this is something we should never do—we should never loop through a collection and modify it at the same time. Iterator is the solution for that. But why is it?
Let's unpack.
The Problem: Structural Modification During Iteration
When you iterate over a collection and try to modify it directly, Java throws a ConcurrentModificationException. This happens because the collection's internal structure changes while you're traversing it, leading to unpredictable behavior.
// This will throw ConcurrentModificationException
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String item : list) {
if (item.equals("B")) {
list.remove(item); // Boom! 💥
}
}
Enter Iterators: The Solution
Java provides two types of iterators to handle this situation differently:
1. Fail-Fast Iterators (Default Behavior)
Most collections in java.util package uses fail-fast iterators. They immediately throw ConcurrentModificationException if the collection is structurally modified after the iterator is created (except through the iterator's own methods).
Characteristics:
Example with ArrayList:
void main() {
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
for (String item : list) {
System.out.println(item);
list.add("D"); //This is WRONG
}
Iterator<String> iterator = list.iterator();
// This is also WRONG - modifying collection directly
list.add("D"); // Structural modification
iterator.next(); // Throws ConcurrentModificationException
// This is CORRECT - using iterator's remove()
List<String> list2 = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> iter = list2.iterator();
while (iter.hasNext()) {
String item = iter.next();
if (item.equals("B")) {
iter.remove(); // Safe removal through iterator
}
}
}
What Really Happens Inside?
Collections maintain an internal counter called modCount (modification count) that tracks structural modifications. Here's what happens under the hood:
// Simplified internal implementation
public class ArrayList<E> {
private int modCount = 0; // Tracks modifications
public boolean add(E element) {
// ... add logic ...
modCount++; // Increment on structural change
return true;
}
private class Itr implements Iterator<E> {
int expectedModCount = modCount; // Snapshot at creation
public E next() {
checkForComodification(); // Check before operation
// ... return next element ...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
Why Is This Protection Necessary?
Without this fail-fast mechanism, several dangerous scenarios could occur:
Example of what could go wrong without protection:
// Imagine if Java didn't throw the exception
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
// Iterator internally at index 1 (pointing to "B")
// If we remove "B", "C" shifts to index 1
// Iterator moves to index 2, skipping "C" entirely!
2. Fail-Safe Iterators (Concurrent Collections)
Collections in java.util.concurrent package use fail-safe iterators. They work on a copy of the collection, allowing concurrent modifications without throwing exceptions.
Characteristics:
Example with CopyOnWriteArrayList:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.addAll(Arrays.asList("A", "B", "C"));
Iterator<String> iterator = list.iterator();
// This is SAFE - no exception thrown
list.add("D"); // Modification after iterator creation
while (iterator.hasNext()) {
System.out.println(iterator.next()); // Prints A, B, C (not D)
}
Different concurrent collections use different strategies to achieve fail-safe behaviour. For example, CopyOnWriteArrayList does the following:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
public Iterator<E> iterator() {
// Iterator gets current array snapshot
return new COWIterator<E>(getArray(), 0);
}
Every write operation creates a new array copy. Iterators work on the array snapshot they received at creation time. This means:
Best Practices
Key Takeaway
The ConcurrentModificationException is actually your friend—it's Java's way of protecting you from unpredictable behaviour.
Remember: It's not about avoiding the exception at all costs, but about choosing the right tool for your specific use case.
Engineering Manager at Exabyting
3wThanks for the article. I was facing the same issue last week.