What is CopyOnWriteArraySet and how does its copy-on-write mechanism work?
answer
- Backed by CopyOnWriteArrayList, not a hash table
- Uniqueness via equals scan (addIfAbsent), no hashCode
- Write = lock + full array copy + volatile swap
- Reads/iteration lock-free, snapshot, no CME
- Small + read-heavy only; writes are O(n) + allocate
basics
~10 sCopyOnWriteArraySet is a thread-safe Set backed by a CopyOnWriteArrayList. Reads are lock-free, but every write copies the whole underlying array, so it suits small, read-heavy sets.
solid answer
~50 sCopyOnWriteArraySet is a concurrent Set in java.util.concurrent that is implemented on top of CopyOnWriteArrayList. Uniqueness is enforced by linear equals() scans (addIfAbsent), not by hashCode — there are no buckets. Its concurrency model is copy-on-write: the backing array is volatile and effectively immutable; any mutation (add/remove) takes a lock, makes a fresh copy of the entire array with the change applied, and swaps the volatile reference. Reads and iterations take no lock and operate on a snapshot of the array taken at the time the iterator was created, so they never throw ConcurrentModificationException and never see partial updates. The costs: every write is O(n) and allocates a full copy, and iterators are weakly consistent (won't reflect later changes; element-changing operations on the iterator are unsupported). It is therefore meant for small sets that are read far more often than written — a classic example is a listener/observer registry.
code
java · 14 lines// Classic read-mostly use: a listener registry shared by threads
Set<Listener> listeners = new CopyOnWriteArraySet<>();
// rare write (copies the backing array)
listeners.add(myListener);
// frequent, lock-free read + iteration over a snapshot
for (Listener l : listeners) { // never throws ConcurrentModificationException
l.onEvent(event); // safe even if another thread adds/removes meanwhile
}
// Iterator is read-only:
Iterator<Listener> it = listeners.iterator();
// it.remove(); // throws UnsupportedOperationExceptiongo deeper
Knows it is a thread-safe Set in java.util.concurrent and that you would reach for it instead of HashSet across threads.
Explains that writes copy the backing array and reads are lock-free, and that it suits read-heavy use; knows iteration won't throw CME.
Describes the volatile-array swap, addIfAbsent equals-scan uniqueness, snapshot/weakly-consistent iterators, and the O(n) write/allocation trade-off; picks it deliberately for small read-mostly sets.
Reasons about GC/allocation pressure under write bursts, contrasts it with ConcurrentHashMap.newKeySet and synchronizedSet for a given workload, and weighs memory-visibility/consistency guarantees in concurrent designs.
## The need it fills Many programs share a small collection across threads where **reads vastly outnumber writes** — for example a list of event listeners read on every event but changed rarely. A plain `HashSet` is **not thread-safe**; wrapping it with `Collections.synchronizedSet` makes every read take a lock, which is wasteful when writes are rare. **`CopyOnWriteArraySet`** (package `java.util.concurrent`) targets exactly this case. ## What it is built on It is a thin `Set` wrapper around a **`CopyOnWriteArrayList`** — a list whose backing storage is an array. Because it is array-backed and unordered-by-hash, **uniqueness is enforced by scanning**: `add` calls the list's `addIfAbsent`, which walks the array calling `equals` until it finds a match or reaches the end. There are **no hash buckets**, so it uses `equals()` only (never `hashCode()`). ## Copy-on-write, step by step The backing array is held in a **`volatile`** field and is treated as **immutable once published**. Visibility of `volatile` guarantees readers always see a fully constructed array. - **A write** (`add`, `remove`, `clear`) acquires an internal **lock**, allocates a **brand-new array** that is a copy of the old one with the change applied, and then atomically **swaps** the `volatile` reference to point at the new array. The old array is never mutated. - **A read** (`contains`, `size`, iteration) takes **no lock**. It simply reads the current `volatile` array reference and works on it. Because writers never touch an array that readers might be using, readers need no synchronization and **never block writers or each other**. ## Snapshot iteration When you create an iterator, it captures the array reference *at that moment* — a **snapshot**. Subsequent writes create new arrays the iterator never sees. Therefore: - Iteration **never throws `ConcurrentModificationException`** (the fail-safe property). - The iterator is **weakly consistent**: it reflects the set as of its creation, not later changes. - Mutating methods on the iterator (`remove`, `set`, `add`) are **unsupported** and throw `UnsupportedOperationException`. ## The trade-offs - **Reads**: fast and lock-free. - **Writes**: **O(n)** time *and* a full **O(n) memory allocation** every single time — add ten elements one by one and you allocate ten growing arrays. - **Memory churn**: heavy writes create garbage and GC pressure. - **Membership cost**: `contains`/`add` are **O(n)** linear scans (no hashing), unlike `HashSet`'s O(1). ## When to use / avoid **Use** for **small**, **read-mostly** sets where mutations are rare — listener registries, configuration/feature-flag sets, plugin lists. **Avoid** for large sets or write-heavy workloads; there a `ConcurrentHashMap.newKeySet()` (a concurrent hash set with O(1) ops and lock-striped writes) is usually the better concurrent Set. ## Tiny mental model Think of a whiteboard you never erase: to change anything you photocopy the whole board, edit the copy, and hang the new copy in place. Anyone already reading the old copy keeps reading it, unbothered — great if people read constantly and edits are rare, terrible if edits are constant.
- Why does iterating a CopyOnWriteArraySet never throw ConcurrentModificationException?The iterator works on a snapshot — the array reference captured when the iterator was created. Writers create entirely new arrays and swap the reference, so the snapshot is never mutated. There is nothing to detect as a concurrent modification, so iteration is fail-safe (but weakly consistent).
- When would ConcurrentHashMap.newKeySet() be a better choice?When the set is large or writes are frequent. It offers average O(1) add/contains and lock-striped concurrent writes without copying the whole structure, whereas CopyOnWriteArraySet pays O(n) time and a full array allocation per write and O(n) membership checks.
A whiteboard you never erase: to edit, you photocopy the entire board, change the copy, and hang it up. Current readers keep reading the old copy undisturbed.
saying these in an interview costs you the question
- Saying it uses hashCode/buckets — it scans linearly with equals (no hashing).
- Claiming reads take a lock — reads are lock-free on a volatile snapshot.
- Thinking it is good for large or write-heavy sets — every write copies the whole array.
- Believing the iterator reflects concurrent changes or supports remove() — it is a weakly-consistent, read-only snapshot.
- Confusing it with a synchronizedSet wrapper, which locks on every read.