📦 Section 4 · Question #22

How to get a synchronized collection

In Java, the standard mechanism for obtaining a thread-safe collection from an unsynchronized one is using the static factory methods provided by the utility class java.util.Col...


🟢 Junior Level

1. Factory Methods for Synchronized Collections

In Java, the standard mechanism for obtaining a thread-safe collection from an unsynchronized one is using the static factory methods provided by the utility class java.util.Collections:

// Synchronized List wrapper:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// Synchronized Set wrapper:
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());

// Synchronized Map wrapper:
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

Complete Inventory of java.util.Collections Factory Methods:

  • Collections.synchronizedCollection(Collection<T> c)
  • Collections.synchronizedList(List<T> list)
  • Collections.synchronizedSet(Set<T> s)
  • Collections.synchronizedSortedSet(SortedSet<T> s)
  • Collections.synchronizedNavigableSet(NavigableSet<T> s)
  • Collections.synchronizedMap(Map<K, V> m)
  • Collections.synchronizedSortedMap(SortedMap<K, V> m)
  • Collections.synchronizedNavigableMap(NavigableMap<K, V> m)

2. The Golden Rule of Safety: Isolate the Backing Object

[!CAUTION] Never retain or expose a reference to the raw, unsynchronized collection! Always instantiate the backing collection inline directly inside the factory method argument.

// ❌ DANGEROUS (Reference Leak / Aliasing Bug):
List<String> rawList = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(rawList);
// Any thread invoking rawList.add("unsafe") bypasses the lock and destroys thread safety!

// ✅ SECURE (Raw backing collection is unreachable from outside):
List<String> syncList = Collections.synchronizedList(new ArrayList<>());

🟡 Middle Level

1. Internal Architecture: The Decorator Pattern

The Collections.synchronized* methods do not modify the bytecode or state of the passed collection. Instead, they instantiate an internal wrapper class implementing the Decorator Pattern, which delegates every method call to the underlying backing collection inside a synchronized (mutex) block:

// OpenJDK implementation excerpt:
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    final List<E> list; // Backing collection reference

    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }

    public E get(int index) {
        synchronized (mutex) { // Mutual exclusion on every method
            return list.get(index);
        }
    }

    public void add(int index, E element) {
        synchronized (mutex) {
            list.add(index, element);
        }
    }
}

In the parent class SynchronizedCollection, the mutex monitor lock defaults to the wrapper object itself:

final Object mutex;

SynchronizedCollection(Collection<E> c) {
    this.c = Objects.requireNonNull(c);
    this.mutex = this; // Synchronizes on the decorator wrapper instance
}

2. Why iterator() is NOT Automatically Synchronized

The iterator() method in SynchronizedCollection returns an ordinary, unsynchronized fail-fast iterator from the backing collection without holding the monitor lock:

public Iterator<E> iterator() {
    return c.iterator(); // Returns raw iterator; mutex is NOT retained!
}

The Architectural Rationale: Iteration is a multi-step process spanning an arbitrary duration with hundreds or thousands of hasNext() and next() calls. If iterator() automatically acquired the mutex, the collection would have no deterministic way to release it (since Java lacks C++ style stack destructors). If an unhandled exception or early break occurred inside the loop, the lock would remain held indefinitely, causing a system-wide Deadlock.

Therefore, the JDK specification mandates that the developer must explicitly manage the lock monitor during iteration:

List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// MANDATORY THREAD-SAFE ITERATION PATTERN:
synchronized (syncList) {
    for (String item : syncList) {
        System.out.println(item);
    }
}

🔴 Senior Level

1. Thread Safety in Stream API & Spliterators

A frequent, insidious bug in modern Java services is invoking streams on a synchronized collection without an external lock:

List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// ❌ CRITICAL BUG: Throws ConcurrentModificationException under concurrent writes!
syncList.stream()
        .filter(s -> s.startsWith("A"))
        .forEach(System.out::println);

Invoking stream() creates a Spliterator over the backing collection. Like an Iterator, a Spliterator traverses elements incrementally in a non-thread-safe manner. If a concurrent thread mutates syncList while the stream pipeline processes elements, the stream fails fast with ConcurrentModificationException.

Correct Stream API Usage:

synchronized (syncList) {
    syncList.stream()
            .filter(s -> s.startsWith("A"))
            .forEach(System.out::println);
}

2. Package-Private Constructors with Shared Mutexes

Inside OpenJDK’s Collections.java, overloaded package-private constructors accept an explicit external mutex reference:

SynchronizedCollection(Collection<E> c, Object mutex) {
    this.c = Objects.requireNonNull(c);
    this.mutex = Objects.requireNonNull(mutex);
}

This mechanism coordinates locking across parent collections and derived views:

  • When calling syncList.subList(from, to) or syncMap.keySet(), the factory returns a new wrapper instance, passing the exact same mutex reference as the parent collection.
  • Because both the derived view and the parent collection synchronize on the same monitor object, modifying the sublist or iterating its keys is coordinated with operations on the parent list, eliminating data races.

4 Tricky Questions

1. What happens if a thread calls syncList.subList(0, 5) while another thread concurrently modifies the parent syncList? Is the sublist operation thread-safe?

Answer: Yes, it is completely thread-safe. When syncList.subList(from, to) is called, the returned SynchronizedRandomAccessList or SynchronizedList is initialized using the package-private constructor passing this.mutex (the parent list’s lock). Consequently, any read or write on the sublist acquires the exact same monitor lock as mutations on the parent list, ensuring mutual exclusion.


2. Why did the JDK architects leave iterator() without internal synchronization, requiring manual synchronization?

Answer: Acquiring a lock inside iterator() would only protect the creation of the iterator object, not the subsequent loop execution (hasNext() / next()). If the JDK attempted to hold the lock for the entire iteration until hasNext() == false, an unhandled runtime exception, an early break, or an infinite loop inside the caller’s block would leave the lock permanently locked, freezing all threads accessing the collection. By forcing the caller to write synchronized (syncList) { ... }, the lifecycle of the lock is bound cleanly to the developer’s lexical scope.


3. What is the fundamental difference between Collections.synchronizedMap and ConcurrentHashMap during size() evaluation?

Answer:

  • Collections.synchronizedMap: Acquires the global object mutex and invokes map.size(). This delivers Strong Consistency (an exact snapshot), but blocks all writing and reading threads while the count is read.
  • ConcurrentHashMap: Never acquires a global lock. It computes mappingCount() by summing baseCount and the striped CounterCell[] array on-the-fly. This delivers Weak Consistency (an estimate accurate to the nanosecond of execution), but allows unlimited parallel reads and writes to proceed without delay.

4. Why does syncList.stream().filter(...).forEach(...) fail with ConcurrentModificationException under concurrency, and how must it be fixed?

Answer: syncList.stream() creates a Spliterator backed by the unsynchronized raw collection. The stream() call itself acquires the mutex only for the nanosecond it takes to instantiate the stream pipeline. When the terminal operation (forEach) executes, it walks the collection incrementally without holding the lock. If another thread invokes add() or remove() during this traversal, the backing modCount changes, triggering a ConcurrentModificationException. Fix: The entire stream pipeline must be enclosed within an explicit synchronized (syncList) { syncList.stream()... } block.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“Collections.synchronized* methods wrap standard collections using the Decorator Pattern, synchronizing each delegated method call on an internal mutex (synchronized (mutex)). To prevent concurrency bugs, the raw backing collection must never be retained outside the wrapper. While individual method calls are thread-safe, all multi-step iterations (including enhanced for-each loops and Stream API pipelines) must be manually enclosed in an external synchronized (syncCollection) block to prevent ConcurrentModificationException. Derived views like subList() share the parent collection’s mutex, preserving mutual exclusion across views.”

High-Impact Architectural Facts

  • Decorator Mechanism: All public methods delegate to the backing collection inside a synchronized (mutex) block; mutex defaults to this.
  • Iteration Invariant: iterator() and spliterator() do not hold locks; manual synchronization on the collection wrapper is mandatory.
  • Shared Mutex in Views: Derived views (subList(), keySet(), entrySet()) share the parent’s mutex, coordinating concurrent operations between views and parent instances.
  • Stream Vulnerability: Stream processing without an external synchronized block fails with ConcurrentModificationException under concurrent writes.

Common Pitfalls & Red Flags

  • ❌ Retaining a reference to the raw collection: Modifying the raw list directly bypasses the decorator’s lock, causing silent data races.
  • ❌ Iterating or streaming without an external synchronized block: Guarantees ConcurrentModificationException in multi-threaded production.
  • ❌ Assuming Collections.synchronizedMap performs like ConcurrentHashMap: Synchronized wrappers serialize all operations through a single global lock.
  • ❌ Redundantly synchronizing single calls: Wrapping single calls in synchronized (list) { list.add(x); } creates unnecessary double-locking overhead.