When you need a thread-safe collection, what are your options and how do you choose among them?
answer
- Not shared with a writer? Use plain collections
- Map → ConcurrentHashMap (sorted → ConcurrentSkipListMap)
- Producer/consumer → BlockingQueue (bounded = back-pressure)
- Read-mostly list → CopyOnWriteArrayList
- Atomic ops (compute/merge/putIfAbsent) for check-then-act
- Avoid Vector/Hashtable; weakly-consistent iterators, no CME
basics
~20 sFor a shared map, use ConcurrentHashMap, not a synchronized HashMap. For a shared queue, use one of the concurrent queues like ConcurrentLinkedQueue or a blocking queue. Avoid the old Vector/Hashtable. Match the tool to whether you need blocking, ordering, or just safe shared access.
solid answer
~40 sFirst ask whether you even share the collection across threads — if not, use the plain implementations. If you do, prefer the java.util.concurrent classes over wrapping with Collections.synchronizedMap or the legacy Vector/Hashtable, which lock the whole structure and serialize all access. For maps, ConcurrentHashMap allows concurrent reads and fine-grained concurrent writes with atomic methods (compute, merge, putIfAbsent) and weakly-consistent iteration that never throws ConcurrentModificationException. For producer/consumer hand-off use a BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) whose take/put block until ready; for non-blocking shared queues use ConcurrentLinkedQueue. If reads vastly outnumber writes and you can tolerate copy-on-write cost, CopyOnWriteArrayList suits listener lists. Key subtlety: individual operations are atomic, but compound 'check-then-act' sequences still need the atomic combinators or external coordination.
code
java · 14 lines// Atomic compound op (no race):
ConcurrentHashMap<String,Integer> counts = new ConcurrentHashMap<>();
counts.merge("a", 1, Integer::sum); // safe increment
counts.computeIfAbsent("k", k -> load(k)); // compute-once
// Producer/consumer with back-pressure:
BlockingQueue<Task> q = new ArrayBlockingQueue<>(1000);
// producer: q.put(task); // blocks if full
// consumer: Task t = q.take(); // blocks if empty
// RACE: two safe calls are not one atomic action
// if (!counts.containsKey(k)) counts.put(k, 0); // DON'T — use putIfAbsent
String load(String k){ return null; }
static class Task {}go deeper
Knows that plain collections aren't thread-safe and that ConcurrentHashMap exists for shared maps.
Picks ConcurrentHashMap over synchronized wrappers and knows BlockingQueue is for producer/consumer.
Selects across the JUC toolkit by blocking/ordering/read-write profile and uses atomic combinators for compound updates; explains weakly-consistent iteration.
Designs concurrency strategy (confinement vs immutability vs concurrent collections), reasons about back-pressure, contention, and memory visibility, and sets codebase-wide defaults.
## The core problem The everyday collections (`ArrayList`, `HashMap`, `HashSet`…) are **not thread-safe**: if two threads mutate one concurrently, you can get corrupted state, lost updates, or a `ConcurrentModificationException` (CME) — an error thrown when a collection detects it changed during iteration. So when a collection is **shared across threads** and at least one thread writes, you need a thread-safe option. (If it's confined to one thread, or published immutably, you need none — that's the cheapest answer.) ## Three eras of options 1. **Legacy synchronized classes** — `Vector`, `Hashtable`. Every method is `synchronized` on the whole object, so all access is serialized (one thread at a time) — a bottleneck. Considered obsolete; avoid in new code. 2. **Synchronized wrappers** — `Collections.synchronizedList/Map/Set(...)`. They wrap a plain collection so each *single* method takes one global lock. Better than nothing, but still coarse-grained, and you must **manually synchronize while iterating** (iteration isn't covered by the per-method lock), or you risk a CME. 3. **`java.util.concurrent` (JUC)** — purpose-built concurrent collections with finer-grained or lock-free strategies. **Prefer these.** ## The JUC toolkit and when to pick each - **`ConcurrentHashMap`** — the go-to thread-safe map. Reads are largely lock-free; writes lock only a small portion (a bin/bucket), so many threads proceed in parallel. It offers **atomic compound operations**: `putIfAbsent`, `compute`, `computeIfAbsent`, `merge` — use these instead of unsafe `if (!map.containsKey(k)) map.put(...)`. Its iterators are **weakly consistent**: they reflect some state during traversal and **never throw CME**. No `null` keys or values allowed. - **`CopyOnWriteArrayList` / `CopyOnWriteArraySet`** — every mutation copies the whole backing array. Writes are expensive and O(n); reads and iteration are lock-free and snapshot-consistent. Ideal when **reads vastly dominate writes** — classic case: a list of event listeners read on every event, rarely modified. - **Concurrent queues**: - **`ConcurrentLinkedQueue` / `ConcurrentLinkedDeque`** — lock-free, **non-blocking**; `poll` returns `null` when empty. Use for high-throughput shared queues where you don't need to wait. - **`BlockingQueue`** family (`ArrayBlockingQueue`, `LinkedBlockingQueue`, `SynchronousQueue`, `PriorityBlockingQueue`, `DelayQueue`) — `put` blocks when full, `take` blocks when empty. The backbone of **producer/consumer** designs and thread pools. Choose bounded (`ArrayBlockingQueue`, or `LinkedBlockingQueue` with a capacity) to apply back-pressure; `SynchronousQueue` for direct hand-off; `PriorityBlockingQueue`/`DelayQueue` for ordering/scheduling. - **`ConcurrentSkipListMap` / `ConcurrentSkipListSet`** — the concurrent, **sorted** map/set (the thread-safe analogue of TreeMap/TreeSet), O(log n), with NavigableMap range methods. ## The decision flow 1. **Is it shared with a writer?** No → plain collection. 2. **Map?** → `ConcurrentHashMap` (or `ConcurrentSkipListMap` if you need sorted/range). 3. **Producer/consumer hand-off / need to block?** → a `BlockingQueue` (bounded for back-pressure). 4. **Non-blocking shared queue?** → `ConcurrentLinkedQueue`. 5. **Read-mostly list/set (listeners)?** → `CopyOnWriteArrayList/Set`. 6. Reach for legacy `Vector`/`Hashtable`/synchronized wrappers only to interoperate with old code. ## The atomicity pitfall (the senior-level point) Thread safety of *individual* operations does **not** make a *sequence* of them atomic. `if (!map.containsKey(k)) map.put(k, v);` is two safe calls with a race in between — two threads can both see 'absent' and both put. Use the **atomic combinator** (`putIfAbsent`/`computeIfAbsent`/`merge`) instead. Likewise, iterating a synchronized-wrapper collection requires holding its lock for the whole loop. Choosing the right concurrent collection is necessary but not sufficient — you must also use its atomic operations for compound logic.
- Why is `if (!map.containsKey(k)) map.put(k,v)` still wrong on a ConcurrentHashMap?Each call is atomic, but the gap between them is not: two threads can both observe the key absent and both put, losing one update. Use putIfAbsent/computeIfAbsent, which perform the check-and-set atomically.
- What does 'weakly consistent iterator' mean and why is it useful?The iterator traverses elements that existed at some point during iteration and may or may not reflect concurrent modifications, but it never throws ConcurrentModificationException. This lets you iterate a live concurrent collection without locking the whole structure.
- When is CopyOnWriteArrayList a bad choice?When writes are frequent: every mutation copies the entire backing array (O(n) and allocation-heavy), so write-heavy workloads suffer badly. It only pays off when reads vastly outnumber writes, e.g. listener lists.
A synchronized HashMap is a shop with one door and one key — only one customer inside at a time, everyone else queues. ConcurrentHashMap is a department store with many aisles: shoppers in different aisles never block each other, and only the aisle being restocked is briefly roped off. A BlockingQueue is a conveyor belt between a cook and a waiter: the waiter waits if there's no plate, the cook waits if the belt is full.
saying these in an interview costs you the question
- Recommending Vector/Hashtable for new code.
- Thinking ConcurrentHashMap makes check-then-act sequences atomic — only individual ops are atomic.
- Using Collections.synchronizedMap and iterating it without holding the lock (risking CME).
- Putting null keys/values into a ConcurrentHashMap (not allowed).
- Choosing CopyOnWriteArrayList for a write-heavy collection.