How do you choose between CopyOnWriteArraySet, ConcurrentHashMap.newKeySet, and Collections.synchronizedSet for a thread-safe Set?
answer
- Decide by size AND write frequency
- synchronizedSet: one lock, serial, manual iteration lock, fail-fast
- COW: lock-free reads, O(n) copy per write — small read-mostly
- newKeySet: O(1), lock-striped, scales — default concurrent Set
- ConcurrentSkipListSet for sorted concurrent
basics
~10 sPick by read/write ratio and size. CopyOnWriteArraySet for tiny read-mostly sets, ConcurrentHashMap.newKeySet for large or write-heavy concurrent sets, and synchronizedSet only as a simple fallback that locks every operation.
solid answer
~40 sAll three give a thread-safe Set but with very different cost models. Collections.synchronizedSet wraps any Set behind a single lock, so every read and write serializes and you must still manually synchronize during iteration — simplest but lowest concurrency. CopyOnWriteArraySet gives lock-free, snapshot reads but copies the entire backing array on every write and does O(n) equals scans, so it only fits small, read-dominated sets like listener registries. ConcurrentHashMap.newKeySet() is a hash-based concurrent Set with average O(1) add/contains, lock-striped writes, and weakly-consistent iterators; it scales to large sizes and frequent writes. So decide by (1) size and (2) write frequency: tiny + read-mostly → COW; large or write-heavy → newKeySet; quick-and-dirty or legacy → synchronizedSet. Also weigh iteration semantics: synchronizedSet is fail-fast under external locking, the other two are weakly consistent.
code
java · 12 lines// Large / write-heavy concurrent set -> hash-based, O(1), scalable
Set<UserId> activeUsers = ConcurrentHashMap.newKeySet();
activeUsers.add(id); // lock-striped, no full copy
// Small, read-mostly -> copy-on-write
Set<Listener> listeners = new CopyOnWriteArraySet<>();
// Legacy/simple, low contention -> wrapper (lock manually to iterate!)
Set<String> s = Collections.synchronizedSet(new HashSet<>());
synchronized (s) { // REQUIRED for safe iteration
for (String x : s) { /* ... */ }
}go deeper
Knows there are several thread-safe Set choices and that CopyOnWriteArraySet exists for concurrency; can use one when told which.
Distinguishes the three by basic behavior — wrapper lock vs copy-on-write vs concurrent hash — and knows COW is for read-heavy sets.
Selects based on size and read/write ratio, explains the O(n)-copy vs O(1) trade-offs and the manual-iteration-lock requirement of synchronizedSet.
Reasons quantitatively about throughput, contention, and allocation/GC pressure under realistic workloads; factors in iteration consistency guarantees and sorted/range needs; defends the choice and its failure modes at scale.
## Why there are several There is no single "best" concurrent `Set`; each option trades **read cost**, **write cost**, **scalability**, and **iteration semantics** differently. Choosing well means matching the data structure to the **access pattern**. ## Option 1 — `Collections.synchronizedSet(set)` Wraps an existing `Set` (often a `HashSet`) so that **every method holds one intrinsic lock**. - **Pros**: trivial, works with any backing Set, preserves the backing Set's ordering/semantics. - **Cons**: all operations **serialize** on one lock — no read concurrency. **Iteration is not automatically safe**: you must wrap the loop in `synchronized (set) { ... }` yourself, or risk `ConcurrentModificationException`. It is **fail-fast**. - **Use when**: contention is low, the set is small, or you are retrofitting legacy code. ## Option 2 — `CopyOnWriteArraySet` Array-backed (via `CopyOnWriteArrayList`); **reads are lock-free** on a volatile snapshot, **writes copy the whole array**. - **Pros**: zero-cost, never-blocking reads; iterators never throw `ConcurrentModificationException`. - **Cons**: every write is **O(n) time + O(n) allocation**; membership is an **O(n) equals scan** (no hashing); high write rates cause GC pressure. - **Use when**: the set is **small** and **read ≫ write** — listener/observer registries, rarely-changed config or feature-flag sets. ## Option 3 — `ConcurrentHashMap.newKeySet()` A `Set` view backed by a `ConcurrentHashMap` — the modern, general-purpose concurrent hash set. - **Pros**: average **O(1)** `add`/`contains`/`remove`; **lock-striped / CAS** writes so many threads write concurrently; scales to **large** sizes and **high write rates**; **weakly-consistent** iterators that never throw CME. - **Cons**: **no ordering**; iteration is weakly consistent (may or may not reflect concurrent changes); aggregate operations like `size()` are estimates under concurrency. - **Use when**: large sets, frequent concurrent mutation, or simply the **default** concurrent Set today. ## The decision procedure 1. **Single-threaded?** Use plain `HashSet`/`LinkedHashSet`/`TreeSet` — don't pay for concurrency you don't need. 2. **Concurrent, small, read-mostly (writes rare)?** `CopyOnWriteArraySet`. 3. **Concurrent, large or write-heavy?** `ConcurrentHashMap.newKeySet()`. 4. **Need sorted concurrent access?** `ConcurrentSkipListSet` (a separate sorted option). 5. **Just need correctness with minimal effort, low contention?** `Collections.synchronizedSet` — but remember to lock during iteration. ## Iteration semantics matter - `synchronizedSet`: **fail-fast**; throws CME if modified mid-iteration without external locking. - `CopyOnWriteArraySet`: **snapshot**, read-only iterator, never CME, but stale. - `newKeySet`: **weakly consistent**, never CME, may reflect some concurrent changes. Pick the weakest acceptable consistency: snapshot is great for "notify everyone currently registered"; weakly consistent is fine for membership; fail-fast forces you to reason about locking explicitly. ## A quick benchmark intuition For a set of N elements with a write fraction w: COW write cost ≈ N (copy) per write, so total write work ≈ w · N per op — it explodes as N or w grow. newKeySet write cost ≈ constant. synchronizedSet read+write ≈ constant work but **serialized**, so throughput is capped by lock contention, not by per-op cost. This is why COW wins only when w·N is tiny.
- Why must you manually synchronize when iterating a Collections.synchronizedSet?The wrapper synchronizes individual method calls but not a whole iteration loop. The returned iterator is fail-fast, so another thread mutating the set mid-loop triggers ConcurrentModificationException. Wrapping the loop in synchronized(set){...} holds the lock for the entire iteration. The other two implementations avoid this with snapshot/weakly-consistent iterators.
- What is ConcurrentSkipListSet and when would you reach for it?It is a concurrent, sorted Set backed by a skip list, giving O(log n) ordered operations and weakly-consistent iteration. Use it when you need both thread safety and sorted/range access (the concurrent analog of TreeSet), which neither newKeySet nor CopyOnWriteArraySet provides.
synchronizedSet is one shared pen everyone queues for; CopyOnWriteArraySet photocopies the whole ledger on each edit; newKeySet is a big filing cabinet where many clerks open different drawers at once.
saying these in an interview costs you the question
- Recommending CopyOnWriteArraySet for large or write-heavy sets.
- Assuming Collections.synchronizedSet makes iteration safe without external locking.
- Treating synchronizedSet as scalable — it serializes all access on one lock.
- Claiming newKeySet preserves insertion or sorted order — it is unordered; use ConcurrentSkipListSet for sorting.
- Ignoring iteration consistency (fail-fast vs snapshot vs weakly consistent) when it matters to correctness.