Java 22: CopyOnWriteArrayList & When It Wins
easy⏱ 5 mincoursejava
The COW strategy
Every add/remove allocates a new array with the change and atomically replaces the reference. Readers holding an old reference finish their iteration unaffected — snapshot semantics, no ConcurrentModificationException. The trade: write cost is O(n) (array copy), memory churn.
When COW wins
Event listeners: registered once, invoked thousands of times per second. Cache snapshots: periodic refresh, constant reads. Observer lists: tiny lists with occasional changes. Rule of thumb: reads > 100x writes.
Iterators are snapshots
A CopyOnWriteArrayList iterator references the backing array at creation time. Subsequent modifications don't affect iteration — and iterator.remove() throws UnsupportedOperationException. For mutation during iteration, use ArrayList + explicit sync, or ConcurrentLinkedQueue.
Build a COW list + crossover calculator
Implement CowList<T> with add, get, snapshot(). Track the total copyCost (sum of sizes at each write) and readCost (number of reads). Find the crossover: at what read:write ratio does COW beat a copy-on-every-read design?
COW is almost never the right default
If your write rate is anything beyond occasional, COW will hurt. ConcurrentLinkedQueue is better for high-throughput FIFO. ConcurrentHashMap.newKeySet() is better for concurrent sets. COW is a niche tool — know when to reach for it.
Quiz: Why no ConcurrentModificationException
Why don't COW iterators throw CME? Each iterator holds its own snapshot — the array it sees is immutable to it. Later writes create new arrays; the iterator never sees them.