ConcurrentHashMap vs Collections.synchronizedMap, and the… — Cracked Java
// Low-Level Design (LLD / OOD) · Concurrency Considerations in LLD
SeniorSystem Design

ConcurrentHashMap vs Collections.synchronizedMap, and the atomics/RW-lock toolkit.

When shared state lives in a collection or a counter, you rarely need to write locks yourself — java.util.concurrent gives you battle-tested primitives. Knowing which to reach for, and the trade-offs, is core LLD concurrency vocabulary. (Internals are covered in the Collections module; here it's selection under interview time.)

ConcurrentHashMap vs Collections.synchronizedMap

Both are thread-safe, but they differ fundamentally:

Collections.synchronizedMapConcurrentHashMap
LockingOne lock for the whole mapPer-bin / CAS — fine-grained
ConcurrencyReaders block writers and each otherConcurrent reads, mostly-concurrent writes
IterationMust manually synchronized the blockWeakly consistent, no external lock needed
Null keys/valuesAllowedNot allowed
Use whenLegacy / trivial contentionDefault for concurrent maps
// Atomic compound ops are the real win — no external lock:
ConcurrentHashMap<SpotType, Integer> free = new ConcurrentHashMap<>();
free.merge(type, -1, Integer::sum);          // atomic decrement
free.computeIfAbsent(type, t -> loadCount(t));// atomic get-or-compute

Atomics — lock-free counters

For a single number or reference, an atomic beats a lock: it uses a hardware compare-and-set (CAS).

AtomicInteger tokens = new AtomicInteger(capacity);   // rate limiter
boolean allow() {
    int t;
    do { t = tokens.get(); if (t == 0) return false; }
    while (!tokens.compareAndSet(t, t - 1));           // retry on contention
    return true;
}

AtomicReference (and updateAndGet) does the same for objects — useful for lock-free state swaps.

ReadWriteLock and StampedLock

When reads vastly outnumber writes (a cache, a config registry), a ReadWriteLock lets readers run in parallel and only serializes writers.

ReadWriteLock lock = new ReentrantReadWriteLock();
V get(K k) { lock.readLock().lock();  try { return map.get(k); }  finally { lock.readLock().unlock(); } }
void put(K k, V v) { lock.writeLock().lock(); try { map.put(k, v); } finally { lock.writeLock().unlock(); } }

StampedLock (Java 8+) adds an optimistic read mode — read without locking, then validate; even faster for read-heavy paths, but it's not reentrant. Mention it as the optimization when reads dominate.

BlockingQueue — producer/consumer

The go-to for handing work between threads: task schedulers, async loggers, pub/sub, thread pools. It blocks on put when full and take when empty — backpressure for free.

BlockingQueue<Task> queue = new LinkedBlockingQueue<>(1000);
// producer: queue.put(task);        blocks when full
// consumer: Task t = queue.take();  blocks when empty

CopyOnWriteArrayList — read-mostly lists

Every write copies the backing array; reads are lock-free and never see a mutation mid-iteration. Ideal for observer/listener lists that are read on every event but mutated rarely. Avoid it for write-heavy collections — copying is O(n) per write.

List<SpotObserver> observers = new CopyOnWriteArrayList<>();   // safe iteration during notify

Quick selection guide

map, concurrent access ........... ConcurrentHashMap (+ merge/compute)
single counter ................... AtomicInteger / LongAdder
single object swap ............... AtomicReference (CAS)
read-heavy, write-rare map ....... ReentrantReadWriteLock / StampedLock
listener list (read-mostly) ...... CopyOnWriteArrayList
hand work between threads ........ BlockingQueue
Pick the primitive by shape of access

Mark your status