Once a collection becomes shared across threads, the next question is usually:

  • should I wrap it with synchronization, or
  • should I replace it with a concurrent collection

Those are not equivalent moves.

Both can be correct in the right setting, but they optimize for different things:

  • synchronized wrappers give simple coarse-grained safety
  • concurrent collections give more scalable, specialized concurrency behavior

The Two Families

Synchronized wrappers

Examples:

  • Collections.synchronizedList(...)
  • Collections.synchronizedMap(...)
  • Collections.synchronizedSet(...)

These wrap an ordinary collection and serialize access through one monitor.

Concurrent collections

Examples:

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • ConcurrentLinkedQueue
  • ConcurrentSkipListMap
  • BlockingQueue implementations

These are designed with concurrency semantics as part of their core behavior.


Problem Statement

Suppose a service shares one in-memory collection across many threads.

You need to decide:

  • is one lock around every operation acceptable
  • or do reads and writes need to overlap more efficiently
  • or does the collection need specialized iteration or queue semantics

If you answer only “I need thread safety,” you are not answering enough. You also need to answer:

  • what kind of workload is this collection under

That determines whether the wrapper or the specialized concurrent structure is the better fit.


Mental Model

A synchronized wrapper says:

  • take one ordinary collection
  • guard each method with one shared monitor

This is conceptually simple and often correct.

A concurrent collection says:

  • the data structure itself implements a concurrency strategy appropriate for its shape

That can mean:

  • more concurrent reads
  • weaker but useful iteration guarantees
  • atomic map helpers
  • non-blocking queue operations
  • blocking producer-consumer semantics

So concurrent collections are not just “faster synchronized wrappers.” They usually have different semantics.


Runnable Comparison Example

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class WrapperVsConcurrentCollectionDemo {

    public static void main(String[] args) {
        Map<String, Integer> synchronizedMap =
                Collections.synchronizedMap(new HashMap<>());
        synchronizedMap.put("P100", 10);
        synchronizedMap.put("P200", 20);

        synchronized (synchronizedMap) {
            for (Map.Entry<String, Integer> entry : synchronizedMap.entrySet()) {
                System.out.println("Wrapped map entry: " + entry.getKey() + "=" + entry.getValue());
            }
        }

        ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
        concurrentMap.put("P100", 10);
        concurrentMap.merge("P100", 5, Integer::sum);
        concurrentMap.computeIfAbsent("P300", ignored -> 30);

        concurrentMap.forEach((key, value) ->
                System.out.println("Concurrent map entry: " + key + "=" + value));
    }
}

The contrast is important:

  • the synchronized wrapper still needs external synchronization for iteration
  • the concurrent map provides concurrency-aware operations and weakly consistent traversal

These are different programming models.


Where Synchronized Wrappers Fit Well

Strong fits:

  • low-contention shared collections
  • simple legacy code where one monitor is enough
  • cases where full iteration can safely hold one external lock
  • small data structures whose throughput demands are modest

Advantages:

  • easy mental model
  • behavior close to the underlying collection
  • minimal API learning cost

If contention is low and the code is simple, wrappers are often perfectly reasonable.


Where Concurrent Collections Fit Well

Strong fits:

  • read-heavy shared maps
  • lock-free queues
  • sorted concurrent access
  • listener lists with rare mutation
  • producer-consumer handoff

Advantages:

  • better scaling under contention
  • collection-specific atomic methods
  • iteration behavior designed for concurrent use
  • specialized queue semantics such as blocking or non-blocking progress

The key is that each concurrent collection earns its place by matching a workload shape.


The Iteration Trap

This is the most common source of confusion.

With synchronized wrappers, this is not enough:

for (String key : synchronizedMap.keySet()) {
    ...
}

You usually need:

synchronized (synchronizedMap) {
    for (String key : synchronizedMap.keySet()) {
        ...
    }
}

Why?

Because the wrapper synchronizes individual methods, but iteration spans many method calls and must still be protected as one traversal boundary.

Concurrent collections often avoid this exact pattern by offering iterators that are safe to use concurrently, but those iterators usually come with weaker consistency guarantees than a fully locked traversal.


Common Mistakes

Assuming a wrapper and a concurrent collection are interchangeable

They may both be thread-safe, but they behave differently under iteration, compound actions, and contention.

Choosing a concurrent collection without needing its semantics

If one synchronized boundary would be simpler and fast enough, extra complexity may be unnecessary.

Forgetting compound actions

Even with thread-safe collections, logic like:

  • check then put
  • get then update

still needs an atomic boundary, either external or built into the collection API.

Treating iteration guarantees as stronger than they are

Weakly consistent iteration is safe for concurrent use, but it is not the same thing as a fully locked snapshot.


Decision Guide

Choose a synchronized wrapper when:

  • contention is low
  • one global lock is acceptable
  • you want a simple safety story

Choose a concurrent collection when:

  • contention is meaningful
  • the collection type offers specialized concurrency behavior you actually need
  • iteration or compound operations should work in a collection-aware way

If you need:

  • blocking semantics, use a blocking queue
  • lock-free FIFO buffering, use ConcurrentLinkedQueue
  • read-heavy concurrent mapping, use ConcurrentHashMap
  • read-mostly snapshot iteration, use CopyOnWriteArrayList

Key Takeaways

  • Synchronized wrappers add one coarse-grained monitor around an ordinary collection.
  • Concurrent collections are purpose-built data structures with workload-specific concurrency semantics.
  • Wrapper iteration still typically requires external synchronization.
  • Choose based on workload shape and semantics, not just on the generic label “thread-safe.”

Next post: ConcurrentHashMap in Java Deep Dive

Comments