Concurrent Collections

Learning goals

By the end of this chapter you should be able to:

  • Choose ConcurrentHashMap, CopyOnWriteArrayList, and blocking queues for concurrent workloads
  • Contrast synchronized wrappers vs purpose-built concurrent structures
  • Use ConcurrentHashMap bulk operations: compute, merge, reduce
  • Understand weakly consistent iterators and why fail-fast differs

Not every synchronized wrapper scales

Wrapping HashMap:

Map<String, String> m = Collections.synchronizedMap(new HashMap<>());

Every get/put acquires the same monitor. Under high contention, threads queue on one lock — correct but slow.

ConcurrentHashMap allows concurrent reads and scoped writes — default for shared mutable maps.


ConcurrentHashMap

ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();

counts.merge("hits", 1, Integer::sum);

counts.computeIfAbsent("session", k -> new Session(k));  // atomic for key

Null policy: no null keys or values — NullPointerException.

Atomic compound ops on a single key — but check-then-act across keys still needs external coordination:

// Still racy if two keys must stay consistent together
if (map.containsKey(a) && map.containsKey(b)) { ... }

Iteration is weakly consistent — reflects state at some point, no ConcurrentModificationException, may or may not see concurrent updates.


CopyOnWriteArrayList / CopyOnWriteArraySet

On write, copy entire backing array. Reads never lock — great when reads vastly dominate.

List<Listener> listeners = new CopyOnWriteArrayList<>();

listeners.add(this::onEvent);  // copies array
listeners.forEach(Listener::handle);  // iterates snapshot

Poor choice for frequent writes — each add is O(n) copy.


Concurrent linked structures

ConcurrentLinkedQueue — lock-free MPMC queue, offer/poll.

LinkedBlockingQueue / ArrayBlockingQueue — blocking producer–consumer (see collections chapter).

PriorityBlockingQueue — thread-safe priority queue.

Pick blocking when threads must wait; concurrent linked when non-blocking retry or another layer handles back-pressure.


ConcurrentHashMap-backed Set

No ConcurrentHashSet in the JDK:

Set<String> unique = ConcurrentHashMap.newKeySet();
// or
Set<String> fromMap = Collections.newSetFromMap(new ConcurrentHashMap<>());

Same concurrency semantics as the backing map.


synchronized vs concurrent — summary

Scenario Prefer
Shared cache map ConcurrentHashMap
Rare writes, many readers of listener list CopyOnWriteArrayList
Producer–consumer buffer LinkedBlockingQueue
Legacy single-lock simplicity synchronizedMap / synchronizedList
Need sorted concurrent map ConcurrentSkipListMap

BlockingDeque and work stealing

LinkedTransferQueue, SynchronousQueue — specialized handoff patterns (executor internals).

ForkJoinPool uses work-stealing deques for divide-and-conquer — pairs with parallelStream and recursive tasks.

Know they exist; default business logic rarely constructs them directly.


Common mistakes

  1. Iterator remove on CopyOnWriteArrayList during for-each of same list — supported on iterator but understand snapshot semantics.
  2. Using size() then put in non-atomic check — use ConcurrentHashMap atomic methods.
  3. ConcurrentHashMap values collection iteration — views are weakly consistent; not a point-in-time snapshot of all values.
  4. Blocking queue add on full queue — throws; use offer or put.
  5. Replacing all synchronized blocks with CHM — compound invariants spanning keys still need locks.

Practice checkpoint

  1. Global dedupe set under concurrent adds — implementation? Answer: ConcurrentHashMap.newKeySet() or Collections.newSetFromMap(new ConcurrentHashMap<>()).

  2. 1000 threads read config list, once/day admin adds entry — list type? Answer: CopyOnWriteArrayList.

  3. ConcurrentHashMap iterator throws CME on concurrent put? Answer: no — weakly consistent, no fail-fast CME.

  4. Rate-limit 5 concurrent DB calls — which j.u.c type? Answer: Semaphore(5).


Interview angles

  • CHM segmentation/CAS: finer than one global map lock; Java 8+ bin trees under heavy collisions.
  • Copy-on-write trade-off: memory + copy cost vs read lock freedom.
  • Blocking queue back-pressure: put blocks producers — stabilizes memory under burst load.
  • When synchronized wrapper wins: low contention, legacy code, need Collections.synchronizedMap simplicity.

Next: /java/learn/concurrency/virtual-threads/