📦 Section 4 · Question #21

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 read get(1) and Thread 3 cannot write via add("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*:

  1. 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 ConcurrentHashMap or CopyOnWriteArrayList, locking the entire data structure from the outside for a multi-step batch update is impossible without introducing separate external locks.

  2. Frequent Random-Access Indexed Writes: The java.util.concurrent package intentionally lacks a “ConcurrentArrayList”. If a workload demands frequent mutations and reads by index (list.get(i), list.set(i, val)), CopyOnWriteArrayList is unusable due to $O(N)$ array copying. In this specific scenario, Collections.synchronizedList(new ArrayList<>()) remains the standard JDK solution.

  3. Interoperability with Legacy APIs: When integrating with third-party libraries that require standard List or Map interfaces 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:

  1. Lock Target: Vector synchronizes every method on this (its own instance). SynchronizedList synchronizes on a dedicated internal final Object mutex, allowing custom mutex objects to be supplied via its constructor.
  2. Growth Factor: When capacity is exceeded, Vector doubles its buffer size ($2.0\times$ growth, or by a custom capacityIncrement), whereas ArrayList inside SynchronizedList grows by $1.5\times$ (newCapacity = oldCapacity + (oldCapacity >> 1)), reducing memory waste.
  3. Design Pattern: Vector is a concrete legacy class; SynchronizedList is a decorator wrapper that can wrap any arbitrary List implementation (including LinkedList).

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.
  • CopyOnWriteArrayList solves 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 external synchronized (collection) blocks to avoid race conditions and ConcurrentModificationException. Modern applications should almost always choose java.util.concurrent classes (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-each loops are not internally locked; iterating without an external synchronized (list) block will throw ConcurrentModificationException under concurrency.
  • Compound Action Vulnerability: if (!map.containsKey(k)) map.put(k, v) releases the lock between calls; requires an external synchronized block.
  • Aliasing Bug: Exposing or mutating the underlying backing collection directly completely bypasses the synchronized wrapper.

Common Pitfalls & Red Flags

  • ❌ Using Collections.synchronizedMap in high-throughput systems: Causes severe thread contention; use ConcurrentHashMap.
  • ❌ 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; requires ConcurrentHashMap.merge().
  • ❌ Using CopyOnWriteArrayList for frequent writes: Incurs $O(N)$ copying overhead; Collections.synchronizedList is vastly more efficient for write-heavy indexed lists.