Concurrent Collections
java.util.concurrent provides collection implementations like ConcurrentHashMap, CopyOnWriteArrayList, and the BlockingQueue family, purpose-built for safe concurrent access with far less contention than wrapping an ordinary collection in synchronized blocks.
Ordinary collections like HashMap and ArrayList are not thread-safe: concurrent modification from multiple threads can corrupt their internal structure or throw ConcurrentModificationException. Wrapping them with Collections.synchronizedMap()/synchronizedList() makes individual operations safe but serializes all access behind one lock, which can become a severe bottleneck. java.util.concurrent's dedicated concurrent collections — ConcurrentHashMap, CopyOnWriteArrayList, the BlockingQueue implementations, and ConcurrentLinkedQueue — are purpose-built for concurrent access patterns, each optimized for a different read/write shape.
Why Not Just Synchronize a HashMap?
Collections.synchronizedMap(new HashMap<>()) makes each individual method call (get, put, etc.) atomic by synchronizing on a single internal lock, but that means every thread accessing the map — even multiple threads only reading it — contends for that one lock, serializing all access and eliminating any real concurrency. It also still requires external synchronization for compound operations (like check-then-put) and for iteration, since iterating while another thread mutates the same synchronized map still risks ConcurrentModificationException unless the whole iteration is itself synchronized on that lock.
ConcurrentHashMap
ConcurrentHashMap is a Map implementation designed for high-concurrency access: reads generally do not block, and writes are synchronized at a much finer granularity than a single whole-map lock (the exact internal locking/striping strategy is a JVM/implementation detail that has evolved across Java versions). It also provides atomic compound operations directly as methods, avoiding the need for external synchronization around common check-then-act patterns.
merge(), computeIfAbsent(), and putIfAbsent() are each performed atomically by ConcurrentHashMap itself, safely handling concurrent calls for the same key without the caller needing any explicit lock.
Map is not a subtype of Collection, so ConcurrentHashMap (like HashMap) does not implement Collection or Iterable<E> directly — iteration happens over its keySet(), values(), or entrySet() views, same as with any other Map.
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
// Atomic "increment or initialize" -- no external synchronization needed
counts.merge("apples", 1, Integer::sum);
counts.computeIfAbsent("bananas", k -> 0);
counts.putIfAbsent("cherries", 1);Weakly Consistent Iteration
Iterators from ConcurrentHashMap (and the other concurrent collections in this topic) are weakly consistent: they never throw ConcurrentModificationException, and they are guaranteed to reflect the collection's state at some point during the iteration, but they do not guarantee reflecting every single modification made concurrently while iterating — an iteration might or might not see an element added or removed by another thread partway through. This is a deliberate, documented tradeoff, not a bug: it favors safety and progress over a strict, frozen snapshot.
Weakly consistent iteration is specifically designed to never fail with ConcurrentModificationException, unlike a plain HashMap's fail-fast iterator.
Thread A: iterating over map.entrySet() --------------------->
Thread B: put("x", 1) remove("y")
^ ^
A's iteration MAY or MAY NOT observe these changes,
depending on exactly when they happen relative to A's traversal —
but A never throws ConcurrentModificationException because of them.CopyOnWriteArrayList
CopyOnWriteArrayList implements List and is optimized for the read-heavy, write-rare case: every mutating operation (add, set, remove) creates and swaps in an entirely new copy of the underlying array, while reads (get, iteration) never need any locking at all and operate on a stable, unchanging snapshot array. This makes reads and iteration extremely cheap and never subject to ConcurrentModificationException — but makes writes expensive, since every single write copies the entire backing array.
A classic use case: an observer/listener list that is iterated frequently (to notify listeners) but modified rarely (adding/removing a listener), where the cost of copying on write is easily outweighed by lock-free, always-safe iteration.
Because iteration works over a fixed snapshot taken when the iterator was created, an iterator from CopyOnWriteArrayList will never reflect additions or removals made after it was created — this is a stronger, more predictable guarantee than ConcurrentHashMap's weak consistency, but specific to this one collection.
import java.util.concurrent.CopyOnWriteArrayList;
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("listenerA");
listeners.add("listenerB");
// Safe to iterate even while another thread concurrently adds/removes listeners --
// this iteration sees a fixed snapshot from when it began, never CME.
for (String listener : listeners) {
System.out.println("notifying " + listener);
}BlockingQueue: Producer/Consumer Coordination
java.util.concurrent.BlockingQueue<E> extends Queue<E> and adds operations that block: put(e) blocks if the queue is currently full (for bounded queues), waiting until space is available; take() blocks if the queue is empty, waiting until an element is available. This directly implements the classic producer/consumer pattern without any manual wait/notify or Condition bookkeeping.
put()/take() give correct, efficient producer/consumer coordination out of the box -- comparable to the ReentrantLock + two-Condition bounded buffer built manually in the Locks topic, but already implemented and tested.
Common implementations: ArrayBlockingQueue (fixed-capacity, array-backed), LinkedBlockingQueue (optionally bounded, linked-node-backed), PriorityBlockingQueue (unbounded, orders elements by natural ordering or a Comparator), and SynchronousQueue (zero capacity — every put() must be matched by a concurrent take(), directly handing off an element between two threads).
import java.util.concurrent.*;
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10); // bounded, capacity 10
// Producer thread:
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put("item-" + i); // blocks once the queue is full
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();
// Consumer thread:
new Thread(() -> {
try {
while (true) {
String item = queue.take(); // blocks while the queue is empty
System.out.println("consumed " + item);
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();ConcurrentLinkedQueue
ConcurrentLinkedQueue<E> is an unbounded, non-blocking, lock-free FIFO queue built on CAS-based operations (similar in spirit to the atomic classes' compare-and-set approach). Unlike BlockingQueue implementations, it has no blocking put()/take() — offer() and poll() simply succeed or return immediately (poll() returns null if empty rather than waiting). It suits high-throughput scenarios where producers and consumers poll rather than block, and where an unbounded queue is acceptable.
poll() returning null instead of blocking is the key contrast with BlockingQueue.take(): callers must handle 'nothing available right now' themselves rather than being automatically suspended until something arrives.
import java.util.concurrent.ConcurrentLinkedQueue;
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
queue.offer(1);
queue.offer(2);
System.out.println(queue.poll()); // 1, or null if the queue were empty -- never blocksChoosing Among the Concurrent Collections
The right choice depends on the read/write ratio and whether blocking (waiting for capacity/availability) or non-blocking (immediate offer/poll) semantics fit the workload.
ConcurrentHashMap key-value map, high concurrent read/write, atomic compound ops
CopyOnWriteArrayList List, read-heavy / write-rare, cheap safe iteration, expensive writes
BlockingQueue impls producer/consumer handoff, blocking put()/take()
ConcurrentLinkedQueue unbounded, non-blocking, lock-free FIFO, poll-based accessConcurrent Collections vs. synchronizedX Wrappers
Collections.synchronizedList/synchronizedMap remain useful for simple, low-contention cases or when a straightforward drop-in wrapper is enough, but the dedicated concurrent collections generally scale far better under real contention because they avoid a single coarse-grained lock, and several of them additionally provide atomic compound operations (merge, computeIfAbsent, putIfAbsent) that a synchronized wrapper does not give you for free — using those still requires manually synchronizing on the wrapper externally for check-then-act sequences.
Practical Usage Summary
Use ConcurrentHashMap as the default thread-safe map for shared, frequently-read-and-written key-value data. Use CopyOnWriteArrayList for small, read-dominated lists like listener/observer registries. Use a BlockingQueue implementation (commonly LinkedBlockingQueue or ArrayBlockingQueue) to hand off work between producer and consumer threads, which is also exactly the internal mechanism ExecutorService thread pools use to queue submitted tasks. Use ConcurrentLinkedQueue when a lock-free, non-blocking, unbounded FIFO queue fits better than blocking semantics.
Key Takeaways
Concurrent collections are purpose-built for thread-safe access with much less contention than synchronizing an ordinary collection behind one lock.
ConcurrentHashMap supports high-concurrency reads/writes and atomic compound operations (merge, computeIfAbsent, putIfAbsent); Map is still not a subtype of Collection.
Iterators from concurrent collections are typically weakly consistent — never throwing ConcurrentModificationException, but not guaranteed to reflect every concurrent modification.
CopyOnWriteArrayList makes reads/iteration essentially free and always safe (fixed snapshot per iterator) by copying the entire backing array on every write — good for read-heavy, write-rare lists only.
BlockingQueue implementations provide blocking put()/take() for producer/consumer coordination without manual wait/notify.
ConcurrentLinkedQueue is an unbounded, lock-free, non-blocking FIFO queue using offer()/poll() rather than blocking operations.
Map<String,Integer> map = Collections.synchronizedMap(new HashMap<>()); for (String key: map.keySet()) {... } // NOT synchronized externally -- risk of CME synchronized (map) { for (String key: map.keySet()) {... } // must externally synchronize iteration too } // or better: use ConcurrentHashMap, whose iterators never throw CME
Iterating a synchronizedMap requires the caller to manually synchronize on the map itself for the whole iteration, or a concurrent modification during iteration can still throw ConcurrentModificationException; ConcurrentHashMap avoids this entirely with weakly consistent iterators.
if (!map.containsKey(key)) { map.put(key, computeValue()); // race: another thread could put() between these two calls } map.putIfAbsent(key, computeValue()); // or better, computeIfAbsent to avoid computing needlessly // map.computeIfAbsent(key, k -> computeValue());
Even on a ConcurrentHashMap, a separate containsKey() then put() is not atomic as a pair — putIfAbsent() or computeIfAbsent() perform the check-then-act atomically as a single operation.
CopyOnWriteArrayList<Integer> hotList = new CopyOnWriteArrayList<>(); for (int i = 0; i < 1_000_000; i++) { hotList.add(i); } // extremely slow: copies the array every add()
List<Integer> hotList = Collections.synchronizedList(new ArrayList<>()); // or a non-concurrent list built once, then published // CopyOnWriteArrayList is for read-heavy, write-RARE workloads, not bulk writes
CopyOnWriteArrayList copies the entire backing array on every single mutating call, making it a poor fit for write-heavy workloads; it is meant specifically for lists that are read/iterated far more often than they are modified.
Why can a single Collections.synchronizedMap() wrapper become a severe bottleneck under high concurrency, compared to ConcurrentHashMap?
synchronizedMap() serializes every operation — including concurrent reads by different threads — behind one single lock, eliminating real concurrency. ConcurrentHashMap is designed for much finer-grained internal synchronization (the exact strategy is implementation-dependent), letting many reads (and often many writes to different parts of the map) proceed with far less contention.
What does 'weakly consistent iteration' mean for a concurrent collection's iterator, and what guarantee does it explicitly NOT make?
It means the iterator never throws ConcurrentModificationException and reflects the collection's state at some point during the traversal, but it does not guarantee reflecting every single concurrent modification made by other threads while the iteration is in progress — an element added or removed concurrently may or may not be seen by that particular iteration.
Why is CopyOnWriteArrayList well suited to a listener/observer list but poorly suited to a list with frequent writes?
Every mutating operation copies the entire backing array and swaps it in, which is cheap relative to reads only when writes are rare — reads and iteration, by contrast, are essentially free since they never lock and operate on a stable snapshot array. A write-heavy workload pays the full array-copy cost on every single mutation, making it far slower than alternatives designed for frequent writes.
What is the difference between put()/take() on a BlockingQueue and offer()/poll() on a ConcurrentLinkedQueue?
put() blocks if a bounded BlockingQueue is full (waiting for space) and take() blocks if it's empty (waiting for an element) — the calling thread suspends until the operation can proceed. ConcurrentLinkedQueue's offer()/poll() never block: offer() adds immediately (the queue is unbounded), and poll() returns immediately, returning null if the queue happens to be empty rather than waiting.
Why is map.containsKey(key) followed by map.put(key, value) still not safe on a ConcurrentHashMap, even though each individual call is thread-safe?
Even though each individual method call is internally atomic, the two-call sequence as a whole is not — another thread could call put() for the same key in between the containsKey() check and this thread's put() call, so the combined check-then-act is a race condition. putIfAbsent() or computeIfAbsent() perform the equivalent logic as one atomic operation, closing that race.
Name two BlockingQueue implementations and one key difference between them.
ArrayBlockingQueue is a fixed-capacity, array-backed bounded queue (capacity set at construction and never grows). LinkedBlockingQueue is backed by linked nodes and can be constructed either bounded (with a specified capacity) or effectively unbounded (no capacity argument), giving it more flexibility in sizing than ArrayBlockingQueue.
Is Map (and therefore ConcurrentHashMap) a subtype of Collection in Java?
No. Map is a separate interface hierarchy from Collection; ConcurrentHashMap, like HashMap, does not implement Collection or Iterable<E> directly. Iteration over a map happens through its keySet(), values(), or entrySet() view collections instead.
Write a thread-safe word-frequency counter using ConcurrentHashMap and merge(), suitable for multiple threads counting words concurrently.
import java.util.concurrent.ConcurrentHashMap; class WordCounter { private final ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>(); void recordWord(String word) { counts.merge(word, 1, Integer::sum); // atomic: initialize to 1, or add 1 to existing count } int getCount(String word) { return counts.getOrDefault(word, 0); } } merge(word, 1, Integer::sum) atomically either inserts the key with value 1 (if absent) or applies Integer::sum to combine the existing value with 1, as a single indivisible operation — safe for any number of threads calling recordWord() concurrently for the same or different words, with no lost updates.
What does this print, and why doesn't the added element 4 show up in the iteration?
Prints '1 2 3 '. CopyOnWriteArrayList's iterator is created over a fixed snapshot of the backing array taken at the moment the iterator() call happened; list.add(4) creates and swaps in a brand new backing array, but the already-created iterator still holds a reference to the original (pre-add) array snapshot, so it never sees the 4 — and importantly, this never throws ConcurrentModificationException.
Write a simple producer/consumer example using LinkedBlockingQueue where a producer thread adds 5 integers and a consumer thread consumes and prints them, then both threads finish cleanly using a sentinel value.
import java.util.concurrent.*; public class ProducerConsumer { private static final int SENTINEL = -1; public static void main(String[] args) throws InterruptedException { BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); Thread producer = new Thread(() -> { try { for (int i = 1; i <= 5; i++) { queue.put(i); } queue.put(SENTINEL); // signal completion } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread consumer = new Thread(() -> { try { int value; while ((value = queue.take()) != SENTINEL) { System.out.println("consumed: " + value); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); producer.start(); consumer.start(); producer.join(); consumer.join(); } }
The producer put()s 1 through 5 then a SENTINEL (-1) value; the consumer take()s values in order (blocking if the queue is momentarily empty) and stops as soon as it reads the sentinel, printing 'consumed: 1' through 'consumed: 5'. Both threads terminate cleanly after join() returns, since the sentinel gives the consumer a well-defined stopping point instead of blocking forever on an empty queue.
List<Integer> list = new CopyOnWriteArrayList<>(List.of(1, 2, 3));
Iterator<Integer> it = list.iterator();
list.add(4);
while (it.hasNext()) {
System.out.print(it.next() + " ");
}