Java Fail Fast Iterator: Behavior and Pitfalls
java fail fast iterator: Learn how Java fail fast iterators detect concurrent modification, the modCount mechanism, and practical ways to avoid ConcurrentModificationE...
Java fail fast iterator is a behavior implemented by most collection iterators in the Java Collections Framework. When you create an iterator over a collection and then modify the collection structurally (by adding or removing elements) from another thread or even from the same thread outside the iterator's own remove method, the next call to the iterator throws ConcurrentModificationException. This is not a bug; it is a deliberate design decision to detect concurrent modification early and fail quickly rather than produce undefined behavior.
What Does Fail Fast Mean for Java Iterators?
A fail fast iterator is one that checks the collection's modification state at each iteration step. If the collection has been modified after the iterator was created, the iterator immediately throws ConcurrentModificationException. The term "fail fast" refers to the system's preference to stop with a clear error rather than continue with potentially corrupted data. This behavior is present in the iterators of ArrayList, HashMap, LinkedList, and other standard collections, unless they are explicitly designed to be fail safe.
The key point is that the iterator does not try to handle the modification gracefully. It fails at the earliest possible moment, which makes bugs visible during development instead of causing subtle issues in production.
The Mechanism Behind Fail Fast: modCount
The implementation relies on a field called modCount in each collection. This counter is incremented whenever the collection's structure is modified, such as when elements are added, removed, or cleared. When an iterator is created, it captures the current modCount value. At each call to next() or hasNext(), the iterator compares the collection's current modCount with the value it captured. If they differ, it throws ConcurrentModificationException.
Here is a simplified illustration of the check inside a typical iterator:
public E next() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } // ... return the next element }
The expectedModCount is set in the iterator's constructor. The iterator's own remove() method updates expectedModCount after removing an element, so that the iterator remains valid after a removal it performs itself.
When Does ConcurrentModificationException Occur?
The exception occurs when a structural modification happens after the iterator is created. This can happen in two common scenarios:
- Modification from another thread while one thread is iterating.
- Modification from the same thread using the collection's
addorremovemethods directly, instead of the iterator's ownremove.
For example, this code throws an exception:
List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (item.equals("a")) { list.remove(item); // Direct removal, not iterator.remove() } }
The list.remove() increments modCount, but the iterator's expectedModCount remains unchanged. The next call to iterator.next() detects the mismatch and throws.
Avoiding Fail Fast Exceptions in Single-Threaded Code
In a single-threaded environment, the most straightforward way to avoid the exception is to use the iterator's own remove() method when you need to delete elements during iteration. This method updates the iterator's internal expectedModCount to match the new modCount.
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (item.equals("a")) { iterator.remove(); // Safe } }
If you need to add elements while iterating, you cannot use the iterator's API because it has no add method. In that case, collect the elements to add in a separate list and add them after the iteration completes, or use a ListIterator for lists, which supports add().
Another approach is to iterate over a copy of the collection. For example:
for (String item : new ArrayList<>(list)) { if (condition) { list.remove(item); } }
This works because you are iterating over a separate snapshot, and modifications to the original list do not affect the copy's iterator.
Fail Fast vs Fail Safe Iterators
Not all iterators are fail fast. Some collections, such as CopyOnWriteArrayList and ConcurrentHashMap, provide fail safe iterators. These iterators operate on a snapshot of the collection taken at the time the iterator is created. They do not throw ConcurrentModificationException when the underlying collection is modified after the iterator is created. Instead, they reflect the state of the collection at the time the iterator was created, which can be useful in concurrent scenarios.
The tradeoff is that fail safe iterators often have higher memory overhead because they rely on copying or versioning, and they may not reflect the latest state of the collection. Fail fast iterators, on the other hand, are cheaper in the common case but are not safe for concurrent modification.
| Aspect | Fail Fast Iterator | Fail Safe Iterator |
|---|---|---|
| Behavior on modification | Throws ConcurrentModificationException | Does not throw; works on snapshot |
| Common collections | ArrayList, HashMap, LinkedList | CopyOnWriteArrayList, ConcurrentHashMap |
| Overhead | Low | Higher (copy or versioning) |
| Use case | Single-threaded or explicit synchronization | Concurrent access without explicit locking |
Performance and Operational Considerations
Fail fast iterators add a small overhead: each call to next() or hasNext() performs an integer comparison between modCount and expectedModCount. This is negligible in most applications. The real benefit is operational: it catches bugs early. If a collection is modified during iteration, you get a clear exception instead of unpredictable behavior like ArrayIndexOutOfBoundsException or infinite loops.
In production, fail fast behavior helps you identify race conditions and logic errors quickly. However, it is not a substitute for proper synchronization. If you need to iterate over a collection while another thread may modify it, you should either use a concurrent collection with fail safe semantics or synchronize the iteration and the modification on the same lock.
Handling Concurrent Modifications in Practice
When you need to allow concurrent modification, choose the right tool for the job. For a list that is read frequently and modified rarely, CopyOnWriteArrayList provides fail safe iteration with acceptable performance. For a map, ConcurrentHashMap offers weakly consistent iterators that do not throw ConcurrentModificationException.
If you must use a standard collection, you can synchronize the block that performs both the iteration and the modification:
synchronized (list) { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (item.equals("a")) { list.remove(item); } } }
This ensures that no other thread can modify the collection while you are iterating. The fail fast iterator will not throw because the modCount changes are only made by the same thread that holds the lock, and the iterator's expectedModCount is updated if you use iterator.remove(). If you use list.remove() inside the synchronized block, the iterator will still throw because the iterator's expectedModCount is not updated. So you must use the iterator's own removal method.
Another pattern is to collect the elements you want to remove and then remove them after the iteration:
List<String> toRemove = new ArrayList<>(); for (String item : list) { if (item.equals("a")) { toRemove.add(item); } } list.removeAll(toRemove);
This avoids modifying the collection during iteration entirely.
Understanding how fail fast iterators work and when they throw is essential for writing reliable Java code. The modCount mechanism is simple, but it has a significant impact on how you structure loops that modify collections. By choosing the right collection type and using the iterator's own methods, you can avoid the exception and keep your code predictable.