Fail-Fast vs Fail-Safe
Fail-fast and fail-safe are two competing philosophies for how Java iterators and collections respond to structural modifications made to a collection while it is being iterated. A fail-fast iterator detects concurrent structural modification via a modCount check and immediately throws ConcurrentModificationException, making the bug visible at the point it occurs. A fail-safe iterator (technically: weakly consistent or snapshot-based) either iterates over a copy of the collection or tolerates concurrent modifications without throwing, at the cost of possibly not reflecting the most recent state of the collection. This entry covers the modCount mechanism in full, what counts as a structural modification, the ConcurrentModificationException contract, all standard fail-fast and fail-safe collections, weakly consistent semantics, snapshot semantics, practical consequences for multi-threaded and single-threaded code, and when each approach is appropriate.
The modCount Mechanism and ConcurrentModificationException
// ── modCount in action — single-threaded CME ─────────────────────────
List<String> list = new ArrayList<>(List.of("a", "b", "c", "d"));
Iterator<String> it = list.iterator(); // captures expectedModCount = 0
it.next(); // OK — modCount == expectedModCount
list.add("e"); // modCount becomes 1
// it.next(); // ConcurrentModificationException — 1 != 0
// ── Safe removal pattern: use iterator's own remove() ─────────────────
List<String> mutable = new ArrayList<>(List.of("alpha", "beta", "gamma", "delta"));
Iterator<String> safeIt = mutable.iterator();
while (safeIt.hasNext()) {
String s = safeIt.next();
if (s.startsWith("b")) {
safeIt.remove(); // increments both modCount AND expectedModCount → safe
}
}
System.out.println(mutable); // [alpha, gamma, delta]
// ── UNSAFE: modifying list outside the iterator ───────────────────────
List<String> bad = new ArrayList<>(List.of("a", "b", "c"));
for (String s : bad) { // enhanced-for uses an iterator
if (s.equals("b")) {
bad.remove(s); // ConcurrentModificationException!
}
}
// ── SAFE alternatives to the above ───────────────────────────────────
// Option 1: removeIf (Java 8+) — no external iterator involved:
List<String> opt1 = new ArrayList<>(List.of("a", "b", "c"));
opt1.removeIf(s -> s.equals("b"));
System.out.println(opt1); // [a, c]
// Option 2: collect to remove list:
List<String> opt2 = new ArrayList<>(List.of("a", "b", "c"));
List<String> toRemove = opt2.stream()
.filter(s -> s.equals("b"))
.collect(Collectors.toList());
opt2.removeAll(toRemove);
System.out.println(opt2); // [a, c]
// ── HashMap structural vs non-structural modifications ────────────────
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
// Non-structural — replacing existing value via Map.Entry.setValue() is allowed:
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.setValue(entry.getValue() * 10); // OK — modCount unchanged
}
System.out.println(map); // {one=10, two=20, three=30}
// Structural — adding a new key during iteration:
try {
for (Map.Entry<String, Integer> entry : map.entrySet()) {
map.put("four", 4); // structural — ConcurrentModificationException
}
} catch (ConcurrentModificationException e) {
System.out.println("CME: " + e.getMessage());
}Fail-Safe Iterators — Weakly Consistent and Snapshot
// ── Weakly consistent: ConcurrentHashMap ─────────────────────────────
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.put("a", 1);
chm.put("b", 2);
chm.put("c", 3);
// Safe to modify during iteration — no CME:
for (Map.Entry<String, Integer> entry : chm.entrySet()) {
System.out.println(entry);
chm.put("d", 4); // no ConcurrentModificationException
}
// "d" may or may not appear during this iteration — weakly consistent
// ── Snapshot: CopyOnWriteArrayList ────────────────────────────────────
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>(
List.of("x", "y", "z"));
Iterator<String> cowIt = cowList.iterator(); // snapshot taken here: [x, y, z]
cowList.add("w"); // new internal array: [x, y, z, w]
// Iterator still sees original snapshot:
while (cowIt.hasNext()) {
System.out.print(cowIt.next() + " "); // x y z — "w" not visible
}
System.out.println();
System.out.println(cowList); // [x, y, z, w] — collection has "w"
// ── CopyOnWriteArrayList: remove() on iterator throws ─────────────────
Iterator<String> cowIt2 = cowList.iterator();
cowIt2.next();
try {
cowIt2.remove(); // UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Cannot remove via COW iterator");
}
// ── Performance trade-off: COW vs synchronized list ───────────────────
// CopyOnWriteArrayList is appropriate when reads vastly outnumber writes:
// Each write = full array copy → O(n) per write
// Each read/iteration = no locking, no copy → O(1) per element
// Synchronizing a standard list for comparison:
List<String> synced = Collections.synchronizedList(new ArrayList<>());
// Must hold lock for iteration:
synchronized (synced) {
for (String s : synced) {
System.out.println(s); // held lock prevents all concurrent mutations
}
}
// COW needs no lock for iteration at all — readers never block
// ── ConcurrentLinkedQueue — weakly consistent, lock-free ─────────────
ConcurrentLinkedQueue<Integer> clq = new ConcurrentLinkedQueue<>();
clq.addAll(List.of(10, 20, 30, 40, 50));
// Can add/remove from other threads mid-iteration without CME:
for (Integer n : clq) {
System.out.print(n + " ");
clq.offer(n + 100); // safe — no CME; may or may not appear in iteration
}Summary Table and Choosing the Right Collection
// ── Decision matrix: which collection/approach to use ────────────────
//
// Use case | Collection / Approach
// ------------------------------------|------------------------------------------
// Single-threaded, no mod during iter | ArrayList, LinkedList, HashMap (any std.)
// Single-threaded, remove during iter | ArrayList + iterator.remove() / removeIf()
// Single-threaded, add during iter | ListIterator.add() or collect then addAll
// Multi-thread, read >> write | CopyOnWriteArrayList / CopyOnWriteArraySet
// Multi-thread, balanced read/write | ConcurrentHashMap, ConcurrentLinkedQueue
// Multi-thread, need sorted order | ConcurrentSkipListMap / Set
// Multi-thread, need full consistency | synchronized block + Collections.synchronized*
// ── removeIf — clean single-threaded modification ────────────────────
List<String> data = new ArrayList<>(
List.of("keep", "remove_me", "keep_too", "remove_this"));
data.removeIf(s -> s.startsWith("remove"));
System.out.println(data); // [keep, keep_too]
// ── replaceAll — clean single-threaded in-place transformation ────────
List<String> words = new ArrayList<>(List.of("hello", "world", "java"));
words.replaceAll(String::toUpperCase);
System.out.println(words); // [HELLO, WORLD, JAVA]
// ── ReadWriteLock for consistent iteration under concurrent writes ─────
List<String> shared = new ArrayList<>(List.of("a", "b", "c"));
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// Reader thread — acquires read lock, others can read concurrently:
Runnable reader = () -> {
rwLock.readLock().lock();
try {
for (String s : shared) {
System.out.print(s + " ");
}
} finally {
rwLock.readLock().unlock();
}
};
// Writer thread — acquires write lock, blocks all readers/writers:
Runnable writer = () -> {
rwLock.writeLock().lock();
try {
shared.add("d");
} finally {
rwLock.writeLock().unlock();
}
};
// ── Verifying fail-fast is NOT a thread-safety mechanism ──────────────
List<Integer> notThreadSafe = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// Two threads accessing without sync: data races, not just CME.
// CME is incidental — the real problem is undefined behavior from
// unsynchronized access to a non-thread-safe structure.
// NEVER rely on CME as a signal that threading is "handled".