Fail-fast iterators throw ConcurrentModificationException (CME) as soon as they detect structural modification during iteration. Fail-safe iterators tolerate concurrent modification by either iterating a snapshot or being weakly consistent.
Fail-fast
Most JDK collections — ArrayList, LinkedList, HashMap, HashSet, TreeMap — produce fail-fast iterators. They track a modCount and throw CME if any structural change is detected between iterator construction and the next operation.
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
for (Integer i : list) {
if (i == 2) list.remove(i); // CME on next iteration
}
The fix is iterator.remove() or list.removeIf(...).
Fail-safe
Two flavors:
Snapshot — CopyOnWriteArrayList, CopyOnWriteArraySet. The iterator holds a reference to the backing array at the moment it was created. All mutations replace the array; the iterator keeps reading the old one. Cost: every write copies the array.
List<String> log = new CopyOnWriteArrayList<>(List.of("a", "b"));
Iterator<String> it = log.iterator();
log.add("c"); // safe
while (it.hasNext()) System.out.println(it.next()); // a, b — NOT c
Weakly consistent — ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentLinkedQueue. The iterator:
- Never throws CME.
- Reflects state at some point at or after iterator construction.
- May see modifications made after construction (may, not will).
- Is guaranteed not to traverse an element twice.
Comparison
| Property | Fail-fast | Snapshot (COW) | Weakly consistent |
|---|---|---|---|
| Throws CME | yes (best-effort) | no | no |
| Sees concurrent writes | no — explodes | no — frozen view | maybe |
| Memory cost | low | full copy per write | low |
| Thread-safe? | no | yes | yes |
Best-effort, not a guarantee
When to pick which
- Single-threaded mutation during iteration → use
Iterator.remove()orremoveIf(). - Many readers, few writers →
CopyOnWriteArrayList. - Concurrent map access →
ConcurrentHashMap. - Need atomicity across reads? → external lock or
compute/merge.