☕ 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

HashMap
HashMap is the most widely used Map implementation in Java. It stores key-value pairs in a hash table, providing average O(1) performance for get, put, remove, and containsKey operations. Keys are stored in no guaranteed order. HashMap permits one null key and multiple null values. It is not thread-safe. Understanding how HashMap uses hash codes, handles collisions, and resizes is foundational knowledge for writing efficient Java code.
LinkedHashMap
LinkedHashMap extends HashMap and maintains a doubly-linked list running through all its entries, preserving the order in which entries were inserted. Iteration over a LinkedHashMap always returns entries in insertion order. Optionally, it can be constructed in access-order mode where entries are ordered by most-recently accessed, making it the perfect foundation for implementing a Least Recently Used (LRU) cache. LinkedHashMap has slightly higher memory overhead than HashMap due to the extra linked list pointers.
TreeMap
TreeMap is a Red-Black tree implementation of the NavigableMap interface. It stores key-value pairs in sorted order — either the natural ordering of keys (requiring them to implement Comparable) or a custom Comparator provided at construction. All basic operations (get, put, remove, containsKey) are O(log n). TreeMap provides rich navigation operations: finding the closest key, extracting sub-maps, and headMap/tailMap views.
Hashtable
Hashtable is a legacy hash table implementation that predates the Collections Framework. Like Vector for lists, Hashtable was Java's original hash map from Java 1.0. It is thread-safe because all its methods are synchronised, but it does not permit null keys or null values. Hashtable has been retrofitted to implement the Map interface but should never be used in new code — HashMap is the non-concurrent replacement and ConcurrentHashMap is the concurrent replacement.