skip to content

Compare CopyOnWriteArrayList with Collections.synchronizedList and a manually-locked ArrayList. When would you choose each?

level: seniorimportance: should knowfreq 50%

answer

  1. synchronizedList = one mutex on every method, reads serialized
  2. synchronizedList iteration needs manual synchronized(list) block
  3. ReadWriteLock lets reads run in parallel
  4. COW = lock-free reads, O(n) writes, stale snapshot iteration
  5. check-then-act is still racy on synchronizedList

basics

~20 s

synchronizedList wraps a list so every method takes one lock, and you must still lock manually to iterate safely. CopyOnWriteArrayList instead copies the array on writes so reads need no lock and iteration is safe and never throws. Use COW when reads dominate; use synchronizedList or your own lock when writes are frequent.

solid answer

~50 s

All three make a List usable from multiple threads, but trade differently. Collections.synchronizedList wraps each method in synchronized on a single mutex: reads and writes are O(1) but serialized — no two threads run any method at once — and iteration is NOT automatically safe; you must hold the wrapper's monitor in a manual synchronized block around the whole loop, or it can throw ConcurrentModificationException. A manually locked ArrayList is the same idea with your own lock, giving you control (e.g. a ReadWriteLock to let reads run in parallel). CopyOnWriteArrayList takes a different tack: writes copy the whole array (O(n)) but reads and iteration are completely lock-free and never throw, at the cost of seeing a stale snapshot. So: COW for read-mostly, rarely-written, hot-read data (listener lists); synchronizedList for simple low-traffic thread-safety; a hand-rolled ReadWriteLock or a different concurrent structure when writes are frequent and you can't pay the copy.

go deeper

for a junior

Knows synchronizedList exists and that COW is another thread-safe option; may not yet know the iteration caveat.

for a middle

Explains the single-mutex serialization of synchronizedList, the manual-lock-around-iteration requirement, and COW's read-mostly niche.

for a senior

Drives the choice from the read/write ratio and contention, knows ReadWriteLock as the middle path, and flags the check-then-act race on wrappers.

for a principal

Sets organizational guidance: defaults per workload class, when to reach for purpose-built concurrent structures, and how the choice affects latency tails and GC pressure at scale.

## Three ways to share a List across threads A `List` needs help to be used by many **threads** at once because `ArrayList` is unsynchronized. Three classic approaches: ### 1. `Collections.synchronizedList(new ArrayList<>())` This returns a **wrapper** that delegates to the underlying list but wraps **every method** (`get`, `add`, `size`, …) in a `synchronized` block on a single internal **monitor** (lock object). Properties: - Every individual call is **atomic** and thread-safe. - Calls are **fully serialized**: only one thread executes any method at a time — even two unrelated reads can't overlap. This is a throughput bottleneck under contention. - **Iteration is NOT safe by default.** A `for-each` loop calls `iterator()`, `hasNext()`, `next()` as *separate* synchronized calls; between them another thread can mutate the list, and the underlying `ArrayList`'s fail-fast iterator then throws `ConcurrentModificationException`. The documented fix is to manually synchronize on the wrapper for the **whole** loop: ```java synchronized (syncList) { for (E e : syncList) { ... } } ``` ### 2. A manually locked `ArrayList` You hold a plain `ArrayList` and guard it with your own lock. With an intrinsic lock (`synchronized`) this is equivalent to option 1 but under your control. The real reason to do it by hand is to use a **`ReadWriteLock`**: a lock with a *read* mode that many threads can hold simultaneously and a *write* mode that is exclusive. That lets concurrent reads proceed in parallel while writes are still serialized — better read throughput than the single-mutex wrapper — but you own the correctness (lock every access, avoid deadlock, guard compound actions). ### 3. `CopyOnWriteArrayList` The **copy-on-write** strategy: writes lock briefly and replace the whole backing array with an edited copy; **reads and iteration take no lock at all** and never throw. Properties: - Reads are maximally fast and never contend — ideal when reads vastly outnumber writes. - Writes are **O(n)** (full array copy + allocation) — bad when writes are frequent or the list is large. - Iteration is a **snapshot**: safe, never throws, but possibly **stale**. ## Decision guide | Need | Best choice | Why | |---|---|---| | Read-mostly, rarely written, hot reads (listeners, config) | `CopyOnWriteArrayList` | lock-free reads, safe iteration, write cost paid rarely | | Low-traffic, simplest thread-safety | `synchronizedList` | one line, correct enough when contention is low (remember to lock iteration) | | Frequent reads **and** writes, parallel reads matter | manual `ReadWriteLock` over an `ArrayList`, or a different structure | avoids COW's per-write copy while still letting reads overlap | | Need to mutate *while* iterating | hand-locked list or explicit copy | COW's snapshot iterator forbids `iterator.remove()` | ## Common pitfalls - **Iterating a `synchronizedList` without an external lock** — the #1 latent bug; it throws CME under load. - **Compound check-then-act on `synchronizedList`** (e.g. `if (!list.contains(x)) list.add(x)`) is still racy: each call is atomic but the pair isn't. You must wrap the pair in `synchronized (list)`. - **Reaching for COW under write-heavy load** — quietly turns every write into an allocation and an O(n) copy, devastating GC and latency. ## Mental model - `synchronizedList` = one bathroom, one key: everyone queues, even to just peek. - `ReadWriteLock` ArrayList = a reading room: many may read together, but writing clears the room. - `CopyOnWriteArrayList` = a published edition: readers read the current printing freely; changing anything means printing a whole new edition.

  • Why can iterating a Collections.synchronizedList still throw ConcurrentModificationException?
    Each iterator call is individually synchronized, but the loop spans multiple calls; another thread can mutate between them, and the underlying ArrayList's fail-fast iterator detects the modCount change. You must hold the wrapper's monitor around the entire loop.
  • How does a ReadWriteLock-guarded ArrayList beat synchronizedList for read-heavy work without COW's copy cost?
    A ReadWriteLock allows many threads to hold the read lock simultaneously, so concurrent reads run in parallel rather than being serialized — and writes don't allocate a full array copy as COW does.

saying these in an interview costs you the question

  • Believing synchronizedList makes iteration thread-safe automatically
  • Thinking synchronizedList lets reads run in parallel (it serializes everything)
  • Recommending COW for write-heavy data
  • Assuming compound operations on a synchronized list are atomic

context