What are fail-fast and fail-safe iterators
When traversing collections in Java, iterators operate under two primary fault-tolerance strategies when facing collection modifications:
🟢 Junior Level
1. Conceptual Definitions
When traversing collections in Java, iterators operate under two primary fault-tolerance strategies when facing collection modifications:
- Fail-Fast: The iterator immediately detects structural modifications (
add,remove,clear,resize) made to the backing collection outside the iterator’s own methods and throws aConcurrentModificationException(CME). - Fail-Safe: The iterator never throws
ConcurrentModificationException, continuing traversal smoothly even if the underlying collection is modified concurrently.
// 1. Fail-Fast (ArrayList, HashSet, HashMap):
List<String> fastList = new ArrayList<>(List.of("A", "B"));
for (String s : fastList) {
fastList.add("C"); // ❌ DISASTER: Throws ConcurrentModificationException!
}
// 2. Fail-Safe (CopyOnWriteArrayList, ConcurrentHashMap):
List<String> safeList = new CopyOnWriteArrayList<>(List.of("A", "B"));
for (String s : safeList) {
safeList.add("C"); // ✅ SAFE: Iterator cleanly completes traversing ["A", "B"]
}
2. Strategy Comparison Matrix
| Criterion | Fail-Fast |
Fail-Safe (Snapshot-Style) |
Weakly Consistent |
|---|---|---|---|
Throws CME? |
✅ Yes (On any concurrent change) | ❌ Never | ❌ Never |
| Example Collections | ArrayList, HashSet, HashMap |
CopyOnWriteArrayList, CopyOnWriteArraySet |
ConcurrentHashMap, ConcurrentLinkedQueue |
| Backing Data Traversed | Live data array directly | Immutable array snapshot | Live nodes via volatile links |
| Observes Concurrent Writes? | Crashes before observing | ❌ No (Isolated in past snapshot) | ⚠️ May observe updates (no guarantee) |
| Mutation Overhead | Zero ($O(1)$) | High on writes ($O(N)$ array cloning) | Minimal (Lock-Free / CAS) |
🟡 Middle Level
1. The Fail-Fast Engine: modCount vs. expectedModCount
Fail-fast collections track their structural mutation history using an internal counter:
protected transient int modCount = 0;
Every operation that alters collection size or internal structure increments modCount++.
When an iterator is created, it captures the current value into a local field:
int expectedModCount = modCount;
At every subsequent step inside next() and remove(), the iterator validates this invariant:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
If the collection was modified directly (e.g., list.add("X")), modCount was incremented, while the iterator’s expectedModCount remained stale. Calling it.next() fails fast and throws ConcurrentModificationException.
[!NOTE] When an element is deleted using the iterator’s own method (
it.remove()), the iterator synchronizes both counters:expectedModCount = modCount. No exception is thrown.
2. JDK Specification Terminology
[!IMPORTANT] The term “Fail-Safe” is not an official term in the Java Language Specification (JLS) or Javadoc; it is industry jargon.
The official OpenJDK documentation divides non-failing iterators into two formal categories:
- Snapshot-Style Iterators: Traverse an immutable, frozen snapshot of the collection created at iterator instantiation (
CopyOnWriteArrayList).- Weakly Consistent Iterators: Traverse live collection nodes safely, never throwing
ConcurrentModificationException, guaranteed to traverse elements present at creation, and never returning duplicates (ConcurrentHashMap).
🔴 Senior Level
1. Why Fail-Fast is “Best-Effort” and NOT a Thread-Safety Guarantee
The OpenJDK Javadoc for ArrayList contains a critical architectural warning:
“The fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification… Fail-fast iterators should be used only to detect bugs.”
The Hardware & Memory Model Reason:
The field modCount is defined as a standard primitive int; it is NOT declared volatile:
protected transient int modCount = 0; // Notice: NOT volatile!
- If Thread 1 mutates the list on CPU Core 1, the incremented
modCountmay linger in the CPU store buffer or L1/L2 cache lines, not immediately flushed to shared DRAM memory. - If Thread 2 iterates on CPU Core 2, it may read a stale cached
modCount, pass themodCount == expectedModCountcheck, and proceed to iterate across corrupted array indices, causing unpredictable behavior orArrayIndexOutOfBoundsException. - Conclusion: Fail-fast is a single-threaded bug detector, not a multi-threaded concurrency guard.
2. Performance Comparison: iterator.remove() vs. removeIf()
When removing elements matching a predicate from an ArrayList, using iterator.remove() introduces severe quadratic overhead:
// ❌ QUADRATIC OVERHEAD: O(N^2)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().startsWith("obsolete_")) {
it.remove(); // Every call executes System.arraycopy to shift the tail!
}
}
// ✅ LINEAR OPTIMIZATION: Strictly O(N) in Java 8+
list.removeIf(s -> s.startsWith("obsolete_"));
Why removeIf() is Superior:
ArrayList.removeIf() uses a two-pass algorithm with a BitSet:
- It traverses the array once, executing the predicate and recording the indices of elements to remove into a compact bitmask.
- It executes a single compacting sweep, shifting surviving elements into place and zeroing out trailing references, performing a single coalesced memory pass.
4 Tricky Questions
1. Can a purely single-threaded application throw ConcurrentModificationException?
Answer:
Yes, and this is the most common cause of the exception.
Despite its name, ConcurrentModificationException has nothing to do with threads. If an application iterates over a collection using an enhanced for-each loop (or explicit Iterator) and calls list.add() or list.remove() directly on the collection inside the loop, modCount increments while expectedModCount remains unchanged. The next iteration step detects the mismatch and immediately throws ConcurrentModificationException.
2. Why did the JDK architects intentionally leave modCount as a non-volatile primitive?
Answer:
Declaring a field volatile incurs measurable performance penalties on every write: it prevents compiler instruction reordering and forces CPU memory barriers (StoreLoad/StoreStore fences) to flush write buffers to cache hierarchies.
Standard collections in java.util (like ArrayList and HashMap) were engineered as ultra-fast, single-threaded building blocks. Degrading the performance of millions of single-threaded writes solely to improve error detection in already broken multi-threaded code was deemed an unacceptable engineering trade-off.
3. What happens if another thread deletes the current element while a thread is actively iterating over a ConcurrentHashMap?
Answer:
No exception is thrown.
Iterators in ConcurrentHashMap are Weakly Consistent. The iterator holds a reference to the active Node<K,V>, whose fields (val, next) are declared volatile.
When a concurrent thread deletes that node from the map, the bucket list pointers in the hash table are updated to bypass the node, but the deleted node’s own next pointer remains intact. The iterator continues forward along node.next, successfully completing its traversal without error.
4. How does ArrayList.removeIf() achieve $O(N)$ performance compared to $O(N^2)$ using iterator.remove()?
Answer:
iterator.remove() deletes elements one-by-one. Each removal requires invoking System.arraycopy to shift all subsequent elements one index to the left. If $K$ elements are deleted from a list of size $N$, the algorithm shifts memory $K$ times, resulting in $O(K \times N) \approx O(N^2)$ time.
ArrayList.removeIf() scans the list in a single pass ($O(N)$), tracking doomed elements with a BitSet. It then performs an in-place two-pointer compaction, moving each surviving element to its final destination in a single forward pass, nulling out the freed tail references in one operation.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“
Fail-Fastiterators (inArrayList,HashSet,HashMap) abort immediately upon detecting concurrent modifications, throwingConcurrentModificationException. They achieve this by comparing the collection’smodCountwith the iterator’sexpectedModCounton every step. ‘Fail-Safe’ is informal developer slang for Snapshot-style iterators (which iterate over an immutable array clone inCopyOnWriteArrayList) or Weakly Consistent iterators (which traverse live volatile node chains inConcurrentHashMapwithout throwing CME). Fail-fast checks are ‘best-effort’ becausemodCountis notvolatile.”
High-Impact Architectural Facts
- Mechanism:
modCount != expectedModCounttriggersConcurrentModificationExceptioninsidecheckForComodification(). - Single-Threaded Trigger: Calling
list.remove()inside an enhancedfor-eachloop throwsConcurrentModificationException. - Non-Volatile Field:
modCountis non-volatile to prioritize single-threaded write throughput, meaning multi-threaded detection is best-effort. - Safe Single-Threaded Mutator:
Iterator.remove()synchronizesexpectedModCount = modCount, avoiding CME. - Linear Removal: Always use
removeIf()($O(N)$ compacting sweep) instead ofiterator.remove()($O(N^2)$ shifting).
Common Pitfalls & Red Flags
- ❌ Assuming
ConcurrentModificationExceptiononly occurs in multi-threaded programs: Most frequently occurs in single-threaded code due to mutating the collection inside afor-eachloop. - ❌ Relying on fail-fast iterators for thread-safety:
modCountis not volatile; it cannot substitute proper concurrency control. - ❌ Attempting
iterator.remove()inCopyOnWriteArrayList: ThrowsUnsupportedOperationException. - ❌ Expecting snapshot iterators to observe concurrent writes: Snapshot iterators are frozen in time and completely decoupled from subsequent writes.
Related Topics
- What is ConcurrentModificationException — Exception root causes and solutions
- How to properly remove elements during iteration — Removal patterns and anti-patterns
- What is CopyOnWriteArrayList — Snapshot list implementation
- What is ConcurrentHashMap — Weakly consistent map implementation