skip to content

What do the Collections.synchronizedXxx wrappers provide, what are their limits, and when would you prefer the java.util.concurrent collections?

level: seniorimportance: should knowfreq 50%

answer

  1. synchronizedXxx = one global lock per method call
  2. compound ops (check-then-act) NOT atomic — lock externally
  3. must synchronize the whole iteration loop
  4. single lock => contention bottleneck
  5. prefer ConcurrentHashMap (striping + atomic computeIfAbsent) / CopyOnWriteArrayList

basics

~20 s

Collections.synchronizedList/Map wrap a collection so each single method call is thread-safe via an internal lock. But iterating still needs you to lock manually, and a single global lock is slow under load, so concurrent collections like ConcurrentHashMap are usually better.

solid answer

~40 s

synchronizedList, synchronizedMap, synchronizedSet wrap an existing collection and guard every method with synchronization on a single mutex, so individual operations are atomic and thread-safe. Two big caveats: first, compound actions (check-then-act, like 'if absent put') are not atomic — you must synchronize externally on the wrapper. Second, iteration is not thread-safe automatically; you must hold the wrapper's lock for the whole loop, or you risk ConcurrentModificationException. Because all access serializes on one lock, throughput collapses under contention. The modern alternative is java.util.concurrent: ConcurrentHashMap uses lock striping and offers atomic compound ops like computeIfAbsent; CopyOnWriteArrayList gives lock-free reads for read-heavy lists. Prefer these for real concurrency; the synchronized wrappers are mostly legacy.

code

java · 13 lines
java
List<String> safe = Collections.synchronizedList(new ArrayList<>());

// Iteration MUST be guarded by the wrapper's own lock:
synchronized (safe) {
    for (String s : safe) {
        process(s);
    }
}

// Compound action also needs external locking:
synchronized (safe) {
    if (!safe.contains("x")) safe.add("x");
}

go deeper

for a junior

Knows synchronizedList/Map make a collection usable from multiple threads for single operations.

for a middle

Knows individual calls are atomic but iteration needs external synchronization to avoid ConcurrentModificationException.

for a senior

Explains compound-op races, the single-lock scalability limit, and reaches for ConcurrentHashMap/CopyOnWriteArrayList instead.

for a principal

Weighs lock striping vs copy-on-write vs single-mutex tradeoffs, weak consistency semantics, and sets guidance to avoid synchronized wrappers in new code.

## The job of the synchronized wrappers Most standard collections (`ArrayList`, `HashMap`, `HashSet`) are **not thread-safe** — concurrent access from multiple threads can corrupt them. `Collections.synchronizedList(list)`, `synchronizedMap(map)`, and `synchronizedSet(set)` return a wrapper where **every method is synchronized on a single internal lock (mutex)**. So one call like `add` or `get` is atomic relative to other calls — no two threads run a wrapped method at the same time. ```java List<String> safe = Collections.synchronizedList(new ArrayList<>()); safe.add("x"); // each individual call is thread-safe ``` ## Caveat 1: compound operations are NOT atomic Thread safety applies only to **single** method calls. A **check-then-act** sequence spans two calls and is a race: ```java if (!safe.contains("x")) { // call 1 safe.add("x"); // call 2 — another thread may have added "x" in between } ``` Between the two calls another thread can interfere. To make a compound action atomic you must **synchronize externally on the wrapper object** (which is the same mutex the wrapper uses internally): ```java synchronized (safe) { if (!safe.contains("x")) safe.add("x"); } ``` ## Caveat 2: iteration must be manually synchronized Iterating calls many methods (`hasNext`, `next`). The wrapper does **not** lock across the whole loop, so another thread can mutate mid-iteration and cause a `ConcurrentModificationException`. You must guard the entire iteration yourself: ```java synchronized (safe) { for (String s : safe) { ... } // hold the lock for the full loop } ``` This is a famous footgun — people assume "synchronized" means "safe to iterate," and it does not. ## Caveat 3: one global lock = poor scalability Because **every** operation contends for the **same single lock**, threads serialize: only one can touch the collection at a time, even pure readers. Under load this becomes a bottleneck. ## The modern alternative: java.util.concurrent - **`ConcurrentHashMap`** — uses fine-grained locking (lock striping / CAS) so many threads operate concurrently. It exposes **atomic compound operations** (`putIfAbsent`, `computeIfAbsent`, `merge`) so you don't need external synchronization. Its iterators are *weakly consistent* — they never throw `ConcurrentModificationException` and tolerate concurrent updates. - **`CopyOnWriteArrayList` / `CopyOnWriteArraySet`** — every write copies the whole backing array, so reads are lock-free and iterators see a stable snapshot. Excellent for read-heavy, write-rare lists (e.g. event listeners); expensive for write-heavy use. - **`ConcurrentLinkedQueue`, `BlockingQueue` implementations** — for producer/consumer patterns. ## When to use what - **Synchronized wrappers:** legacy code, or trivial low-contention cases where you'll also handle iteration/compound locking. Generally discouraged for new code. - **ConcurrentHashMap:** the default for concurrent maps — better throughput and atomic compound ops. - **CopyOnWriteArrayList:** read-mostly lists where writes are rare. ## Key terms - **Thread-safe:** behaves correctly when accessed by multiple threads simultaneously. - **Mutex / lock:** a guard ensuring only one thread enters a critical section at a time. - **Atomic operation:** appears to happen all-at-once; no other thread sees a half-done state. - **Compound / check-then-act:** an operation made of multiple steps that together must be atomic. - **Lock striping:** splitting one lock into many sub-locks so independent operations don't contend. - **Weakly consistent iterator:** an iterator that reflects some state at/after creation, never throws CME, but may not see the latest updates. - **ConcurrentModificationException:** thrown by fail-fast iterators when the collection is structurally modified during iteration.

  • Why must you synchronize manually when iterating a synchronizedList?
    Each iterator call is individually locked but the loop as a whole is not; another thread can mutate between calls and trigger ConcurrentModificationException. You must hold the wrapper's lock for the entire loop.
  • How does ConcurrentHashMap avoid the single-lock bottleneck?
    It uses fine-grained locking/CAS (lock striping) on parts of the table so many threads write concurrently, plus weakly-consistent iterators that never throw CME.

saying these in an interview costs you the question

  • Believing iteration over a synchronized collection is automatically safe
  • Assuming check-then-act is atomic on a synchronized wrapper
  • Treating synchronizedMap as equivalent in throughput to ConcurrentHashMap
  • Using CopyOnWriteArrayList for write-heavy workloads

context