☕ Java

Synchronized Collections

Synchronized collections wrap standard Java collections to make every individual method call thread-safe via an intrinsic monitor lock. The Collections utility class provides Collections.synchronizedList(), Collections.synchronizedSet(), Collections.synchronizedMap(), and related methods, each returning a wrapper that synchronizes on a single lock object before delegating to the backing collection. While synchronized collections make individual operations atomic, they do not make compound operations atomic and do not protect iteration — callers must externally synchronize iteration and any check-then-act sequences. This entry covers every synchronized wrapper method, the synchronization contract, why iteration still requires external locking, compound operations and the TOCTOU problem, comparison with concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList), the legacy Vector and Hashtable, and when synchronized wrappers are the right tool.

Synchronized Wrapper Methods and the Locking Contract

Collections.synchronizedList(List<T> list) returns a SynchronizedList wrapper. Every method of the wrapper — size(), get(), add(), remove(), contains(), set(), clear(), indexOf(), and so on — acquires the wrapper's intrinsic lock before calling the corresponding method on the backing list and releases it after. The lock object is the SynchronizedList wrapper instance itself by default, but the two-argument form Collections.synchronizedList(list, mutex) allows specifying an external mutex object, enabling coordination with other code that locks on the same mutex. The same pattern applies to: Collections.synchronizedSet(Set<T>), Collections.synchronizedSortedSet(SortedSet<T>), Collections.synchronizedMap(Map<K,V>), Collections.synchronizedSortedMap(SortedMap<K,V>), and Collections.synchronizedCollection(Collection<T>). The underlying class hierarchy is a set of nested static classes in java.util.Collections, each wrapping the corresponding interface with synchronized delegation. The critical contract: each individual method call is atomic with respect to other calls through the same wrapper. Calling size() and calling get(0) are each individually atomic, but the pair size() followed by get(0) is not atomic — another thread could remove an element between the two calls. This is the essence of why synchronized wrappers are insufficient for all thread-safe use cases. Synchronization granularity is one lock for the entire collection. This means all reads block all writes and all other reads. For read-heavy workloads this is a significant throughput bottleneck compared to java.util.concurrent collections, which use finer-grained locking or lock-free algorithms. ConcurrentHashMap, for example, uses a lock per segment (in Java 7) or per bin/node (in Java 8+), allowing multiple writes to proceed concurrently as long as they do not hash to the same bucket.
Java
// ── Creating synchronized wrappers ────────────────────────────────────
List<String>     synList  = Collections.synchronizedList(new ArrayList<>());
Set<String>      synSet   = Collections.synchronizedSet(new HashSet<>());
Map<String,Int>  synMap   = Collections.synchronizedMap(new HashMap<>());
SortedSet<String> synSortedSet = Collections.synchronizedSortedSet(new TreeSet<>());
SortedMap<String,Integer> synSortedMap = Collections.synchronizedSortedMap(new TreeMap<>());

// ── Individual methods are atomic ─────────────────────────────────────
// Two threads calling add() concurrently — both calls are atomic, no data corruption:
synList.add("thread-safe-add");   // acquires lock, adds, releases
synList.size();                   // acquires lock, reads size, releases

// ── Custom mutex — coordinate with external synchronized block ────────
Object sharedMutex = new Object();
List<String> synListCustomMutex = Collections.synchronizedList(
    new ArrayList<>(), sharedMutex);

// Both of these lock on sharedMutex:
synchronized (sharedMutex) {
    synListCustomMutex.add("x");
}
synchronized (sharedMutex) {
    System.out.println(synListCustomMutex.size());
}

// ── Demonstrating thread safety of individual methods ─────────────────
List<Integer> shared = Collections.synchronizedList(new ArrayList<>());
ExecutorService executor = Executors.newFixedThreadPool(10);

// 10 threads each add 1000 elements — no data corruption:
for (int i = 0; i < 10; i++) {
    final int threadId = i;
    executor.submit(() -> {
        for (int j = 0; j < 1000; j++) {
            shared.add(threadId * 1000 + j);
        }
    });
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
System.out.println(shared.size());  // always 10000 — no races on individual add()

// ── Comparison: unprotected ArrayList would give wrong result ─────────
List<Integer> unsafe = new ArrayList<>();
ExecutorService ex2 = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
    ex2.submit(() -> {
        for (int j = 0; j < 1000; j++) unsafe.add(j);
    });
}
ex2.shutdown();
ex2.awaitTermination(5, TimeUnit.SECONDS);
System.out.println(unsafe.size());  // likely != 10000 — data races, corruption

Iteration Requires External Locking — Compound Operations

The most important limitation of synchronized wrappers is that iteration is not protected. The iterator is obtained from the backing collection, not from the wrapper, and it holds no lock during traversal. If another thread modifies the collection while you iterate, the iterator throws ConcurrentModificationException (fail-fast behavior inherited from the backing collection). The Javadoc for Collections.synchronizedList explicitly states: "It is imperative that the user manually synchronize on the returned list when traversing it via Iterator, Spliterator, or Stream." The correct pattern is to synchronize on the wrapper itself (which is the mutex, unless a custom mutex was provided) for the entire duration of iteration. This ensures no other thread can modify the collection through the wrapper while iteration is in progress. The downside is that all other threads are blocked from reading or writing the collection for the entire iteration duration, which can be a significant bottleneck for large collections or slow iteration bodies. Compound operations — check-then-act and read-modify-write sequences — also require external synchronization. Common examples: putIfAbsent semantics (check containsKey, then put), conditional removal (check contains, then remove), and size-based decisions (check size, then add if below a threshold). Each individual call is atomic, but the pair is not. Another thread can interleave between the check and the act. If you find yourself writing synchronized (synList) around a compound operation, that is a signal that a concurrent collection with built-in compound-operation methods (ConcurrentHashMap.computeIfAbsent, ConcurrentHashMap.putIfAbsent) would be both simpler and more efficient. Enhanced-for loops use iterators internally, so they also require external locking: for (String s : synList) must be wrapped in synchronized (synList) { ... }. Streams from synchronized collections similarly require external locking: synchronized (synList) { synList.stream().forEach(...); }.
Java
// ── Iteration MUST be externally synchronized ────────────────────────
List<String> synList = Collections.synchronizedList(
    new ArrayList<>(List.of("a", "b", "c", "d")));

// WRONG — another thread can modify synList mid-iteration → CME:
for (String s : synList) {        // UNSAFE
    System.out.println(s);
}

// CORRECT — lock on the wrapper for the full iteration:
synchronized (synList) {
    for (String s : synList) {    // SAFE
        System.out.println(s);
    }
}

// CORRECT — iterator form, same requirement:
synchronized (synList) {
    Iterator<String> it = synList.iterator();
    while (it.hasNext()) {
        System.out.println(it.next());
    }
}

// CORRECT — streams require the same lock:
synchronized (synList) {
    synList.stream()
           .filter(s -> s.compareTo("b") > 0)
           .forEach(System.out::println);
}

// ── Compound operations — check-then-act is NOT atomic ────────────────
Map<String, Integer> synMap = Collections.synchronizedMap(new HashMap<>());
synMap.put("counter", 0);

// WRONG — two separate atomic operations, not one compound atomic one:
// Thread 1 and Thread 2 both read 0, both write 1 → lost update:
if (synMap.containsKey("counter")) {           // atomic read
    synMap.put("counter",
        synMap.get("counter") + 1);            // atomic read + atomic write
}                                              // NOT compound-atomic!

// CORRECT — synchronize the compound check-then-act externally:
synchronized (synMap) {
    if (synMap.containsKey("counter")) {
        synMap.put("counter", synMap.get("counter") + 1);
    }
}

// BETTER — use ConcurrentHashMap with atomic compound method:
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.put("counter", 0);
chm.compute("counter", (k, v) -> v == null ? 1 : v + 1);  // atomic compound op

// ── putIfAbsent race condition with synchronized wrappers ─────────────
Map<String, List<String>> index = Collections.synchronizedMap(new HashMap<>());
String key = "java";

// WRONG — race between containsKey and put:
if (!index.containsKey(key)) {          // thread A sees false
    index.put(key, new ArrayList<>());  // thread B also puts — one list lost
}

// CORRECT — external synchronization for compound op:
synchronized (index) {
    index.computeIfAbsent(key, k -> new ArrayList<>());
}

// BEST — ConcurrentHashMap.computeIfAbsent is inherently atomic:
ConcurrentHashMap<String, List<String>> concIndex = new ConcurrentHashMap<>();
concIndex.computeIfAbsent(key, k -> new ArrayList<>());

Legacy Synchronized Classes, and Choosing the Right Tool

Before the Collections utility methods existed, Java provided two inherently synchronized collection classes: Vector and Hashtable. Vector is a synchronized, growable array — essentially a thread-safe ArrayList. Hashtable is a synchronized hash table — essentially a thread-safe HashMap. Both synchronize every method on the instance itself. Both are considered legacy and should not be used in new code: they have all the same limitations as synchronized wrappers (iteration requires external locking, compound operations are not atomic), with the additional disadvantage that they cannot be swapped for unsynchronized implementations when single-threaded use is sufficient. The Stack class (extends Vector) and Properties class (extends Hashtable) are similarly legacy. Use ArrayDeque instead of Stack, and ConcurrentHashMap or a Properties-over-HashMap pattern instead of Properties. Choosing between synchronized wrappers and concurrent collections depends on access patterns and required atomicity. Synchronized wrappers are appropriate when: the collection is only occasionally shared between threads, the operations are infrequent, or you need to wrap a specific List or Set implementation (LinkedList, TreeSet) that has no concurrent equivalent. Concurrent collections are preferred when: high throughput under concurrent access is required; you need atomic compound operations (putIfAbsent, computeIfAbsent, merge); or you need snapshot iteration (CopyOnWriteArrayList). The java.util.concurrent package (Java 5+) provides: ConcurrentHashMap (high-concurrency hash map with atomic compound ops), CopyOnWriteArrayList (snapshot iteration, read-heavy workloads), ConcurrentLinkedQueue and ConcurrentLinkedDeque (lock-free queues), and the BlockingQueue implementations (LinkedBlockingQueue, ArrayBlockingQueue, PriorityBlockingQueue) for producer-consumer coordination. These should be the first choice for any new concurrent collection use.
Java
// ── Legacy Vector vs ArrayList + synchronizedList ────────────────────
// Vector — synchronized by default, legacy:
Vector<String> vector = new Vector<>();
vector.add("legacy");
// Every method locks on 'this' — same limitations as synchronizedList

// Modern equivalent:
List<String> modernSynced = Collections.synchronizedList(new ArrayList<>());
// Prefer this if you need a synchronized ArrayList

// ── Stack (legacy) vs Deque ───────────────────────────────────────────
Stack<Integer> legacyStack = new Stack<>();
legacyStack.push(1);
legacyStack.push(2);
System.out.println(legacyStack.pop());   // 2

// Modern equivalent — ArrayDeque is faster, not synchronized:
Deque<Integer> deque = new ArrayDeque<>();
deque.push(1);
deque.push(2);
System.out.println(deque.pop());   // 2

// Thread-safe stack — ConcurrentLinkedDeque:
Deque<Integer> safeDeque = new ConcurrentLinkedDeque<>();
safeDeque.push(1);

// ── When to use synchronized wrappers vs concurrent collections ────────
//
// synchronized wrappers:
//   ✔  Wrapping a specific implementation (e.g., LinkedList, TreeSet)
//   ✔  Low-contention, infrequent concurrent access
//   ✔  Need to use Collections.sort(), binarySearch() — requires List
//   ✘  High contention (one lock = serialized access = poor throughput)
//   ✘  Compound atomic operations (need external sync)
//   ✘  Safe iteration without blocking all writers

// ConcurrentHashMap:
//   ✔  High-throughput concurrent reads and writes
//   ✔  Atomic compound ops: putIfAbsent, computeIfAbsent, merge, compute
//   ✔  Weakly consistent iteration without external locking
//   ✘  Cannot lock the whole map for an atomic multi-step operation easily

// CopyOnWriteArrayList:
//   ✔  Read-heavy: iterations never block, no locking needed
//   ✔  Snapshot iteration — always consistent view
//   ✘  Write-heavy: O(n) copy per write

// ── ConcurrentHashMap atomic compound operations ──────────────────────
ConcurrentHashMap<String, Integer> wordCount = new ConcurrentHashMap<>();

// putIfAbsent — atomic:
wordCount.putIfAbsent("java", 0);

// compute — atomic read-modify-write:
String[] words = {"java", "python", "java", "java", "python"};
for (String w : words) {
    wordCount.merge(w, 1, Integer::sum);  // atomic increment
}
System.out.println(wordCount);  // {java=3, python=2}

// computeIfAbsent — atomic lazy initialization:
ConcurrentHashMap<String, List<String>> groups = new ConcurrentHashMap<>();
groups.computeIfAbsent("even", k -> new CopyOnWriteArrayList<>()).add("2");
groups.computeIfAbsent("even", k -> new CopyOnWriteArrayList<>()).add("4");
System.out.println(groups);  // {even=[2, 4]}

Related Topics in Collections Framework

Collections Overview
The Java Collections Framework (JCF) is a unified architecture for representing and manipulating groups of objects. It provides a set of interfaces that define the operations a collection must support, a set of abstract classes that provide partial implementations, and a set of concrete implementations optimised for different use cases. Every Java developer uses collections daily — lists for sequences, sets for uniqueness, maps for key-value pairs, and queues for ordering — and choosing the right implementation for the right use case is one of the most fundamental practical skills in Java.
Iterable
Iterable<E> is the root interface of the Java Collections hierarchy. Any class that implements Iterable can be used in a for-each loop. It declares a single abstract method: iterator(), which returns an Iterator<E> that the for-each loop uses to traverse the elements. Implementing Iterable is all that is required to make a custom data structure work with Java's enhanced for loop, the Stream API, and any method that accepts an Iterable.
Collection Interface
Collection<E> is the root interface of the main collection hierarchy, extending Iterable<E>. It defines the common operations that all collection types must support: adding elements, removing elements, checking containment, querying size, clearing, converting to an array, and bulk operations. List, Set, and Queue all extend Collection. Map does not extend Collection because a map operates on key-value pairs rather than individual elements.
ArrayList
ArrayList is a resizable-array implementation of the List interface. It is the most commonly used collection in Java, providing dynamic sizing on top of a standard array. Elements are stored in contiguous memory, enabling O(1) random access by index. When the internal array is full, ArrayList automatically allocates a larger array and copies all elements. ArrayList is not thread-safe and preserves insertion order, allowing duplicates.