All topics
library
advanced

CopyOnWrite Collections

Use CopyOnWriteArrayList and CopyOnWriteArraySet for read-heavy, write-rare thread-safe scenarios.

CopyOnWriteArrayList copies the whole backing array on every write. Reads take no lock at all.

Like a shared Google Doc where edits create a new version. Readers always see a consistent version. Editors pay the cost of copying, but readers are never blocked.

Key Concepts

1
    List<Listener> listeners = new CopyOnWriteArrayList<>();
2
    listeners.add(l);                     // locks, copies the array
    for (Listener l : listeners) l.fire(); // no lock, no exception
3
How it works. The array reference is volatile. A write locks, copies, mutates the copy, and publishes the new reference. A read just follows the current reference.
volatile
4
So the cost profile is extreme.
    read     lock-free, as fast as an ArrayList
    write    O(n) -- a full array copy, every time
    memory   two copies during a write
5
Which means one add in a loop is quadratic:
    for (int i = 0; i < 10_000; i++) cowList.add(i);   // 50M element copies
6
Build a plain list and pass it to the constructor instead.
7
Iterators see a snapshot.
    var list = new CopyOnWriteArrayList<>(List.of("a", "b"));
    for (String s : list) list.remove(s);     // no exception, iterates both
8
The iterator holds the array from when it was created, so writes during the loop are invisible to it. It never throws ConcurrentModificationException, and iterator.remove() is unsupported — there is nothing it could sensibly remove from.
ConcurrentModificationExceptioniterator.remove()
9
Versus the alternatives.
    synchronizedList         locks on read AND write
    CopyOnWriteArrayList     free reads, expensive writes
    ConcurrentHashMap        both cheap, but it is a map
10
Where it fits exactly. A listener or observer list: written at startup, read on every event. That is the canonical use, and the JDK uses it that way internally.
11
Where it does not. Anything written during normal operation, anything large, or a cache. A thousand-entry list written once a second copies a million references a second for nothing.
12
CopyOnWriteArraySet is the same mechanism wrapped around the list, so contains is O(n), not O(1). For a set of any size, ConcurrentHashMap.newKeySet() is the better choice.
CopyOnWriteArraySetcontainsConcurrentHashMap.newKeySet()
13
What the interviewer is probing.1. "When is CopyOnWriteArrayList the right choice?" Probing: the narrow fit. Stalls: "For thread safety." Moves up: many reads and very few writes, the canonical case being a listener list written at startup and read on every event.
14
2. "What does a write cost?" Probing: the trade. Stalls: "A lock." Moves up: a full array copy, so building one by adding in a loop is quadratic — construct a plain list and pass it to the constructor.
15
3. "Why does its iterator never throw ConcurrentModificationException?" Probing: the snapshot. Stalls: "It locks during iteration." Moves up: it holds the array from creation, so concurrent writes are invisible to it — and iterator.remove is unsupported for the same reason.
16
4. "Why is CopyOnWriteArraySet a poor large set?" Probing: the underlying structure. Stalls: "It is fine." Moves up: it is backed by the list, so contains is O(n); ConcurrentHashMap.newKeySet is the right choice at size.