When to use synchronized collections?
Imagine a toilet with a single key — while one thread is inside, all others wait in line, even if they just want to check if it's free.
🟢 Junior Level
Synchronized collections are regular collections with a lock on every method.
Imagine a toilet with a single key — while one thread is inside, all others wait in line, even if they just want to check if it’s free.
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
Problem: Only ONE thread can work at a time (even for reading!).
Modern alternative:
// ❌ Old approach (slow)
List<String> list = Collections.synchronizedList(new ArrayList<>());
// ✅ New approach (fast)
Map<String, Integer> map = new ConcurrentHashMap<>();
List<String> list = new CopyOnWriteArrayList<>();
synchronizedList — one lock for ALL operations, including reads. ConcurrentHashMap — lock-free reads and granular locks on writes, so N threads can read in parallel.
🟡 Middle Level
Problem: one lock for everything
// 100 threads want to read
syncList.get(0); // All wait in line!
// → 0 parallelism
Compound operations are NOT atomic
// ❌ Race condition!
if (!syncList.contains(item)) { // Thread A checked
// Thread B slipped in and added
syncList.add(item); // Thread A added a duplicate!
}
// ✅ External synchronized
synchronized (syncList) {
if (!syncList.contains(item)) {
syncList.add(item);
}
}
Iteration requires manual locking
// ❌ ConcurrentModificationException!
for (String s : syncList) { ... }
// ✅ Correct:
synchronized (syncList) {
for (String s : syncList) { ... }
}
When to use
| Scenario | What to choose |
|---|---|
| New code | ConcurrentHashMap |
| Listener lists | CopyOnWriteArrayList |
| Full lock needed | synchronizedList |
| Legacy code | synchronizedList |
COWAL is good for listeners: iteration (traversing all listeners) is more frequent than adding a listener. COWAL provides CME-free iteration and O(1) addition.
🔴 Senior Level
When NOT to use synchronized collections
- High contention (>10 threads) — ConcurrentHashMap will give an order of magnitude better throughput
- Frequent iterations — each requires manual
synchronized(list) { ... } - Compound operations (check-then-act) — still need external synchronized, so why use the wrapper?
Coarse-grained locking
One monitor for the ENTIRE collection:
→ 100 readers → serialization
→ On multi-core CPUs → 0 scalability
ConcurrentHashMap:
→ Lock-free reads
→ CAS + synchronized per bucket
→ 100 threads work in parallel
Double synchronization
// ❌ Double locking!
synchronized (syncList) {
syncList.add(e); // Internal synchronized + external
}
// synchronizedList already synchronizes every method
// → Extra overhead
Exposing the original collection
// ❌ Thread-safety violation!
List<String> original = new ArrayList<>();
List<String> sync = Collections.synchronizedList(original);
// Modification bypassing the wrapper:
original.add("x"); // NOT synchronized!
When synchronized collections are justified
// 1. Legacy systems (pre-Java 5 code)
// 2. Coarse-grained atomicity of the entire collection
synchronized (syncList) {
// Guaranteed nobody can slip in
for (var item : syncList) {
process(item);
}
}
// 3. Low contention + small size
// → Wrapper is cheaper than ConcurrentHashMap
Production Experience
Real scenario: synchronizedMap killed throughput
- API: 1000 RPS, synchronizedMap for cache
- Lock contention: 90% of time on synchronized
- Solution: ConcurrentHashMap
- Result: +900% throughput
Best Practices
- DO NOT use in new code
- ConcurrentHashMap — by default
- CopyOnWriteArrayList — for listeners
- Iteration → synchronized block is mandatory
- Compound operations → external synchronized
- Do not expose the original collection
- Vector/Hashtable → avoid in new code. The only case where they’re tolerable is single-threaded legacy code where replacement isn’t justified by the risks.
Summary for Senior
- Single lock → 0 scalability
- Compound operations → race condition without external synchronized
- Iteration → manual synchronized block
- ConcurrentHashMap > synchronizedMap in 99% of cases
- Legacy → the only justification
- Vector/Hashtable — built-in synchronized → legacy
🎯 Interview Cheat Sheet
Must know:
- Synchronized collections — one lock for all methods (reads + writes)
- Compound operations (check-then-act) are NOT atomic — need external
synchronized - Iteration requires a manual
synchronized(list) { ... }block - ConcurrentHashMap gives lock-free reads and granular locks on writes
- CopyOnWriteArrayList is ideal for listener lists (frequent reads, rare writes)
- Synchronized collections are not recommended in new code
- Exposing the original collection breaks thread-safety
- Vector/Hashtable — legacy, avoid in new code
Frequent follow-up questions:
- Why does synchronizedMap kill throughput at 1000 RPS? — 90% of time is spent on lock contention; ConcurrentHashMap solves the problem.
- Is the construct
if (!list.contains(x)) list.add(x)atomic? — No, it’s two separate calls; an externalsynchronized(list)is needed. - Why would iterating over a synchronizedList without a synchronized block throw CME? — Iteration is a compound operation (hasNext + next), not protected by a single method call.
- When are synchronized collections justified? — Legacy code pre-Java 5, low contention, coarse-grained atomicity of the entire collection.
Red flags (DO NOT say):
- “synchronizedList is a good choice for new code” — no, ConcurrentHashMap/CopyOnWriteArrayList are better
- “Compound operations are atomic in synchronizedList” — no, external synchronized blocks are needed
- “Vector is a normal choice” — it’s a legacy class with coarse-grained locking
- “synchronizedList solves all concurrency problems” — it doesn’t solve race conditions for compound operations
Related topics:
- [[22. How to get a synchronized collection]]
- [[26. What are fail-fast and fail-safe iterators]]
- [[27. What is ConcurrentModificationException]]