☕ Java

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

Fail-fast behavior is implemented via a modCount (modification count) field maintained by every AbstractList, AbstractMap, and their subclasses. modCount is an int incremented on every structural modification to the collection: add, remove, clear, set in some cases, and any operation that changes the size or restructures the internal storage. A non-structural modification — such as set() on an ArrayList at an existing index — does not increment modCount. When an iterator is created, it captures the current modCount into its own field, typically named expectedModCount. On every call to next(), remove(), or (for ListIterator) add() and set(), the iterator checks whether the collection's current modCount equals expectedModCount. If they differ, the iterator throws ConcurrentModificationException immediately, regardless of whether the modification was made by another thread or by the same thread outside the iterator. The contract for ConcurrentModificationException is deliberately best-effort: the Javadoc for Iterator states that fail-fast iterators throw ConcurrentModificationException on a best-effort basis. This means the check is not guaranteed to fire every time a concurrent modification occurs — particularly in multi-threaded scenarios where the modCount write may not be visible to the iterating thread without a happens-before relationship. The check is reliable for single-threaded modifications because all operations occur in the same thread's memory model. For true thread safety, you must use the concurrent collections in java.util.concurrent — fail-fast iterators are not a substitute for synchronization. What constitutes a structural modification varies by collection. For ArrayList: add(), remove(), clear(), and addAll() are structural; set() is not. For HashMap: put() when adding a new key, remove(), clear(), and rehash (resize) are structural; put() replacing an existing key's value is not structural (but Map.Entry.setValue() is allowed during iteration).
Java
// ── 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

Java provides two distinct fail-safe semantics: weakly consistent and snapshot. Both avoid ConcurrentModificationException but differ in how much of the concurrent mutation they reflect. Weakly consistent iterators are used by all java.util.concurrent collections: ConcurrentHashMap, ConcurrentLinkedQueue, ConcurrentSkipListMap, ConcurrentSkipListSet, LinkedBlockingQueue, LinkedBlockingDeque, and others. The weakly consistent guarantee means: the iterator is guaranteed to traverse each element that existed at construction time exactly once; modifications made after the iterator is constructed may or may not be reflected in the traversal; no element will be returned twice and no ConcurrentModificationException will be thrown. This is a formal guarantee, not just a best-effort behavior. For ConcurrentHashMap specifically, the iterator reflects the state of segments it has not yet visited — changes to already-visited segments are not seen. Snapshot iterators are used by CopyOnWriteArrayList and CopyOnWriteArraySet. When an iterator is created, a reference to the current internal array is captured. All traversal operates against that snapshot array. Any subsequent modification to the collection (add, remove, set) creates a brand-new internal array; the iterator continues over the original snapshot and sees none of the modifications. This means snapshot iterators never throw ConcurrentModificationException, always traverse a consistent view, but may reflect stale data. The iterator's remove() method throws UnsupportedOperationException — mutations must go through the collection directly. The cost model differs significantly: weakly consistent iterators have no snapshot overhead — they traverse the live data structure with node-level or segment-level concurrency. Snapshot iterators pay an O(n) copy cost on every write operation, which is why CopyOnWriteArrayList is only appropriate for collections that are read far more often than they are written. For single-threaded code, both fail-fast and fail-safe semantics are available simply by choosing the right collection. The choice should be driven by whether you need to modify the collection during iteration and whether the simplicity of fail-safe semantics outweighs the snapshot or weakly consistent trade-offs.
Java
// ── 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

Choosing between fail-fast and fail-safe collections requires understanding the access patterns, thread-safety requirements, and consistency guarantees needed by the use case. For single-threaded code where you need to modify during iteration, the cleanest solutions are: using the iterator's own remove() (and ListIterator's add/set), using Collection.removeIf(), or using Stream.filter() to produce a new collection. These avoid fail-fast issues without changing the collection type. For multi-threaded read-heavy workloads where writes are rare, CopyOnWriteArrayList and CopyOnWriteArraySet provide the simplest programming model — complete snapshot consistency for readers at the cost of O(n) writes. For multi-threaded workloads with frequent writes and reads, the java.util.concurrent collections (ConcurrentHashMap, ConcurrentLinkedQueue, ConcurrentSkipListMap) provide weakly consistent iteration with high throughput via lock striping or lock-free algorithms. For multi-threaded workloads that require strong consistency during iteration (every modification must be fully visible or fully hidden), an external lock held for the entire duration of iteration is required — either a synchronized block around a synchronized collection, or a ReadWriteLock. No iterator in the JDK provides transactional snapshot semantics under concurrent writes without external synchronization except CopyOnWrite collections. The key misconception to avoid: fail-fast iterators are not a thread-safety mechanism. The CME is a debugging aid for detecting programming errors (modification outside the iterator in single-threaded code, or unsynchronized concurrent access). It does not make the collection thread-safe and should never be used as a substitute for proper synchronization.
Java
// ── 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".

Related Topics in Collections Framework

Collections Overview
The Java Collections Framework (JCF) is a unified architecture for representing and manipulating groups of objects. It provides a set of interfaces that define the operations a collection must support, a set of abstract classes that provide partial implementations, and a set of concrete implementations optimised for different use cases. Every Java developer uses collections daily — lists for sequences, sets for uniqueness, maps for key-value pairs, and queues for ordering — and choosing the right implementation for the right use case is one of the most fundamental practical skills in Java.
Iterable
Iterable<E> is the root interface of the Java Collections hierarchy. Any class that implements Iterable can be used in a for-each loop. It declares a single abstract method: iterator(), which returns an Iterator<E> that the for-each loop uses to traverse the elements. Implementing Iterable is all that is required to make a custom data structure work with Java's enhanced for loop, the Stream API, and any method that accepts an Iterable.
Collection Interface
Collection<E> is the root interface of the main collection hierarchy, extending Iterable<E>. It defines the common operations that all collection types must support: adding elements, removing elements, checking containment, querying size, clearing, converting to an array, and bulk operations. List, Set, and Queue all extend Collection. Map does not extend Collection because a map operates on key-value pairs rather than individual elements.
ArrayList
ArrayList is a resizable-array implementation of the List interface. It is the most commonly used collection in Java, providing dynamic sizing on top of a standard array. Elements are stored in contiguous memory, enabling O(1) random access by index. When the internal array is full, ArrayList automatically allocates a larger array and copies all elements. ArrayList is not thread-safe and preserves insertion order, allowing duplicates.