When to use synchronized collections
This category also includes legacy classes from Java 1.0 — Vector and Hashtable, which enforce synchronization directly inside their method signatures.
🟢 Junior Level
1. What Are Synchronized Collections
Synchronized Collections are decorator wrappers around standard Java Collections Framework classes (ArrayList, HashSet, HashMap), created via static factory methods in java.util.Collections. In these wrappers, every single method invocation is guarded by a coarse-grained mutual exclusion lock on an internal mutex object (synchronized (mutex)):
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
This category also includes legacy classes from Java 1.0 — Vector and Hashtable, which enforce synchronization directly inside their method signatures.
2. The Core Architectural Problem: Serialized Access
Synchronized collections enforce a single global monitor lock across all operations:
- If Thread 1 is reading an element via
get(0), Thread 2 cannot even readget(1)and Thread 3 cannot write viaadd("item"). - On modern multi-core architectures, this completely eliminates concurrency: overall application throughput degrades to the speed of a single CPU core, causing massive thread queueing and context-switching overhead.
In modern Java software development, data structures from the java.util.concurrent package are preferred:
// ❌ OBSOLETE & SLOW (Coarse lock serializes all reads and writes):
Map<String, User> map = Collections.synchronizedMap(new HashMap<>());
// ✅ MODERN STANDARD (Lock-Free reads + fine-grained per-bucket locks):
Map<String, User> map = new ConcurrentHashMap<>();
🟡 Middle Level
1. The Compound Operations Trap (Check-Then-Act Race Condition)
Synchronizing individual methods does not make multi-step transactions atomic:
// ❌ CRITICAL BUG: Check-Then-Act Race Condition!
if (!syncList.contains(user)) {
// Thread can be preempted here!
// Another thread checks and inserts the user during this window.
syncList.add(user); // Duplicate inserted!
}
// ✅ MANDATORY PATTERN: Explicit external synchronization block
synchronized (syncList) {
if (!syncList.contains(user)) {
syncList.add(user);
}
}
Individually, contains() and add() acquire and release the lock. However, the lock is released between the two invocations, leaving a window of vulnerability where concurrent threads can corrupt business invariants.
2. Mandatory Manual Synchronization During Iteration
Calling syncList.iterator() or executing an enhanced for-each loop does not automatically hold the collection lock for the duration of the traversal:
// ❌ CRASHES IN PRODUCTION (ConcurrentModificationException):
for (String item : syncList) {
process(item);
}
// ✅ MANDATORY JDK CONTRACT: Manual synchronization over full iteration
synchronized (syncList) {
for (String item : syncList) {
process(item);
}
}
The method syncList.iterator() returns a standard fail-fast iterator from the backing collection. The lock is held only during the brief invocation of iterator(), not during subsequent calls to hasNext() and next(). If a peer thread mutates the list between steps, modCount changes and the loop throws a ConcurrentModificationException.
🔴 Senior Level
1. When Synchronized Collections Are Still Justified
Despite the superiority of java.util.concurrent, specific engineering scenarios still warrant using Collections.synchronized*:
- Monolithic Coarse-Grained Batch Operations (Whole-Collection Atomicity):
When an application must perform a complex sequence of transformations atomically across the entire list (e.g., clearing, repopulating, and sorting), having a single shared mutex simplifies locking:
synchronized (syncList) { syncList.clear(); syncList.addAll(newBatch); Collections.sort(syncList); }In
ConcurrentHashMaporCopyOnWriteArrayList, locking the entire data structure from the outside for a multi-step batch update is impossible without introducing separate external locks. -
Frequent Random-Access Indexed Writes: The
java.util.concurrentpackage intentionally lacks a “ConcurrentArrayList”. If a workload demands frequent mutations and reads by index (list.get(i),list.set(i, val)),CopyOnWriteArrayListis unusable due to $O(N)$ array copying. In this specific scenario,Collections.synchronizedList(new ArrayList<>())remains the standard JDK solution. - Interoperability with Legacy APIs:
When integrating with third-party libraries that require standard
ListorMapinterfaces and expect serialized synchronization without changing the type hierarchy.
2. The Aliasing / Reference Leak Vulnerability
A severe bug when working with synchronized wrappers is retaining and using a reference to the raw, unsynchronized collection:
List<String> rawList = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(rawList);
// Thread 1 interacts via the wrapper:
syncList.add("alpha");
// Thread 2 accidentally retains and uses rawList:
rawList.add("beta"); // ❌ DISASTER: Mutates list bypassing the synchronization lock!
Direct access to rawList bypasses the mutex, corrupting array state and triggering race conditions.
[!IMPORTANT] Best Practice: Never preserve a reference to the backing collection instance:
List<String> list = Collections.synchronizedList(new ArrayList<>());
3. Architecture Selection Matrix
| Workload / Use Case | Recommended Collection | Why Not Collections.synchronized*? |
|---|---|---|
| Key-Value Dictionary (Map) | ConcurrentHashMap |
Lock-free reads, CAS empty buckets, orders of magnitude higher throughput |
| Unique Elements (Set) | ConcurrentHashMap.newKeySet() |
Lock-free reads, non-blocking bucket distribution |
| Read-Heavy Lists (Listeners/Observers) | CopyOnWriteArrayList |
Zero-lock reads, snapshot iteration without CME |
| Task Queue (Producer-Consumer) | BlockingQueue / ConcurrentLinkedQueue |
Hardware-optimized dual-lock queues or lock-free CAS nodes |
| List with Frequent Random Writes | Collections.synchronizedList |
COWAL causes $O(N)$ allocation thrashing; no ConcurrentArrayList in JDK |
4 Tricky Questions
1. Why does iterating over Collections.synchronizedList(list) throw ConcurrentModificationException if the list is synchronized?
Answer:
A for-each loop compiles into sequential calls to iterator.hasNext() and iterator.next().
While syncList.iterator() is synchronized, it releases the lock immediately upon returning the iterator instance. As the loop iterates, the lock is not held between iterations. If a concurrent thread invokes add() or remove() during traversal, the backing array’s modCount increments. The next call to iterator.next() detects that modCount != expectedModCount and immediately throws ConcurrentModificationException.
To prevent this, the entire iteration loop must be enclosed in an explicit synchronized (syncList) { ... } block.
2. In what structural ways does Vector differ from Collections.synchronizedList(new ArrayList<>())?
Answer:
- Lock Target:
Vectorsynchronizes every method onthis(its own instance).SynchronizedListsynchronizes on a dedicated internalfinal Object mutex, allowing custom mutex objects to be supplied via its constructor. - Growth Factor: When capacity is exceeded,
Vectordoubles its buffer size ($2.0\times$ growth, or by a customcapacityIncrement), whereasArrayListinsideSynchronizedListgrows by $1.5\times$ (newCapacity = oldCapacity + (oldCapacity >> 1)), reducing memory waste. - Design Pattern:
Vectoris a concrete legacy class;SynchronizedListis a decorator wrapper that can wrap any arbitraryListimplementation (includingLinkedList).
3. Is a counter increment atomic in Collections.synchronizedMap: map.put(key, map.get(key) + 1)?
Answer:
No, absolutely not.
This expression consists of three distinct operations: reading the current value (map.get), incrementing the integer in the CPU register, and storing the updated value (map.put).
The map’s lock is acquired and released twice independently. Between get() and put(), a concurrent thread can update or delete the key, causing lost updates. To ensure atomicity, the operation must be enclosed in an external synchronized (map) { ... } block or migrated to ConcurrentHashMap.merge(key, 1, Integer::sum).
4. Why is there no ConcurrentArrayList in java.util.concurrent, and what should be used for concurrent random-access writes?
Answer: Maintaining a dynamic array with $O(1)$ indexed reads and writes under lock-free concurrency is algorithmically problematic:
- Shifting elements during insertions or deletions (
System.arraycopy) invalidates indexed slots across all concurrent threads simultaneously, making lock-free linearizability nearly impossible without coarse locking. CopyOnWriteArrayListsolves this by copying the entire array on writes, but this is only viable when writes are rare. When high-frequency random-access writes by index are required across multiple threads,Collections.synchronizedList(new ArrayList<>())remains the standard JDK solution, or applications partition data across multiple independent sub-lists (striping).
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“
Collections.synchronized*methods wrap standard collections in a decorator that protects every method with a single shared mutex lock (synchronized (mutex)). While this provides basic thread safety, it serializes all operations—including concurrent reads—crippling performance on multi-core systems. Individual method calls are safe, but compound operations (Check-Then-Act) and iterations require manual externalsynchronized (collection)blocks to avoid race conditions andConcurrentModificationException. Modern applications should almost always choosejava.util.concurrentclasses (ConcurrentHashMap,CopyOnWriteArrayList,BlockingQueue), reserving synchronized collections primarily for whole-collection batch atomicity or lists requiring frequent random-access indexed writes.”
High-Impact Architectural Facts
- Coarse Serialization: A single mutex guards the entire collection; reader threads block reader threads and writer threads alike.
- Iteration Invariant: Enhanced
for-eachloops are not internally locked; iterating without an externalsynchronized (list)block will throwConcurrentModificationExceptionunder concurrency. - Compound Action Vulnerability:
if (!map.containsKey(k)) map.put(k, v)releases the lock between calls; requires an externalsynchronizedblock. - Aliasing Bug: Exposing or mutating the underlying backing collection directly completely bypasses the synchronized wrapper.
Common Pitfalls & Red Flags
- ❌ Using
Collections.synchronizedMapin high-throughput systems: Causes severe thread contention; useConcurrentHashMap. - ❌ Iterating over a synchronized collection without external synchronization: A classic production source of
ConcurrentModificationException. - ❌ Assuming
syncMap.put(k, syncMap.get(k) + 1)is thread-safe: It is a non-atomic composite operation; requiresConcurrentHashMap.merge(). - ❌ Using
CopyOnWriteArrayListfor frequent writes: Incurs $O(N)$ copying overhead;Collections.synchronizedListis vastly more efficient for write-heavy indexed lists.
Related Topics
- How to get a synchronized collection — Factory methods and usage patterns
- What is ConcurrentHashMap — Lock-free and fine-grained concurrent map
- What is CopyOnWriteArrayList — Snapshot-based list alternative
- What is ConcurrentModificationException — Mechanics of fail-fast detection