Concurrent Collections: ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueues
Master high-throughput thread-safe data structures in java.util.concurrent, dissecting lock striping, copy-on-write semantics, and producer-consumer queue architectures.
By the end, you’ll be able to…
- Dissect ConcurrentHashMap's internal bin-level locking, CAS updates, and tree bin transformations
- Evaluate trade-offs between CopyOnWriteArrayList and synchronized lists in read-heavy architectures
- Architect resilient producer-consumer pipelines using ArrayBlockingQueue and LinkedBlockingQueue
01ConcurrentHashMap Under the Hood: CAS and Synchronized Bins
In multi-threaded Java applications, using a legacy Hashtable or wrapping a standard HashMap with Collections.synchronizedMap(map) creates a disastrous performance bottleneck: every single read and write operation acquires a single global monitor lock. Concurrent readers are forced to serialize behind writes, destroying CPU multi-core scalability.
ConcurrentHashMap provides high-throughput concurrent access through an ingenious segmented architecture. Since Java 8, ConcurrentHashMap eliminated explicit Segment array locks in favor of node-level synchronization. When putting a key into an empty hash bucket, it uses lock-free Compare-And-Swap (CAS) instructions to install the new node without acquiring any monitor locks.
If a bucket already contains collisions, ConcurrentHashMap synchronizes only on the head node of that specific bucket bin. While one thread inserts into bucket 4, another thread can write to bucket 42 simultaneously without contention. Furthermore, reads (get operations) proceed completely lock-free via volatile field reads.
02CopyOnWriteArrayList: Read-Heavy Invariants
In event-driven architectures and observer patterns, listeners are registered infrequently during application bootstrap, but dispatched thousands of times per second whenever domain events fire. If an event dispatcher iterates over a standard ArrayList while another thread registers a listener, a ConcurrentModificationException is thrown.
CopyOnWriteArrayList solves this specific problem by treating its underlying array as immutable. Any write operation (add, set, remove) creates a fresh copy of the entire internal array, updates the copy, and points the volatile array reference to the new array.
Because existing iterators retain references to the immutable snapshot taken at iterator creation time, read operations never lock and never throw ConcurrentModificationException. However, because copying large arrays incurs O(n) memory allocation and copy overhead on every single write, CopyOnWriteArrayList is strictly suitable only for 99% read / 1% write scenarios.
03BlockingQueue Pipelines for Producer-Consumer Workloads
When decoupling ingestion workloads from downstream persistence, BlockingQueue implementations from java.util.concurrent serve as the foundational backbone. The BlockingQueue interface provides thread-safe put() and take() operations that naturally block when the queue is full or empty, handling backpressure automatically without busy-spinning.
ArrayBlockingQueue is backed by a bounded circular array and uses a single lock with two conditions (notEmpty, notFull). LinkedBlockingQueue employs separate head and tail locks, allowing producers and consumers to enqueue and dequeue concurrently with reduced lock contention.
04Concurrent Collections Selection Matrix
Choosing the right concurrent collection requires balancing write frequency, ordering guarantees, and queue capacity bounds:
Always favor bounded queues over unbounded queues in production. An unbounded LinkedBlockingQueue can silently accumulate millions of pending tasks during a downstream outage until the JVM crashes with an OutOfMemoryError.
+------------------------+------------------+------------------+---------------------------+| Collection | Read Concurrency | Write Cost | Recommended Use Case |+------------------------+------------------+------------------+---------------------------+| ConcurrentHashMap | Lock-free (O(1)) | Bucket lock / CAS| General high-throughput KV|| CopyOnWriteArrayList | Lock-free (O(1)) | High (O(n) copy) | Observer listener lists || ArrayBlockingQueue | Guarded by lock | Guarded by lock | Bounded worker pipeline || LinkedBlockingQueue | Separate lock | Separate lock | Two-lock decoupled queue || ConcurrentLinkedQueue | Lock-free (CAS) | Lock-free (CAS) | Unbounded non-blocking Q |+------------------------+------------------+------------------+---------------------------+The ideas worth keeping.
- ConcurrentHashMap utilizes CAS on empty bins and synchronizes on bucket head nodes to maximize concurrent throughput.
- CopyOnWriteArrayList is ideal for read-dominant observer patterns but prohibitive for frequent write workloads.
- Always bound BlockingQueue capacities in production to apply backpressure and prevent OutOfMemoryError crashes.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Implement an idempotent event counter using C
Implement an idempotent event counter using ConcurrentHashMap and compute(). If the event exists, increment its count; otherwise set it to 1.
Explore a worked solution
import java.util.concurrent.ConcurrentHashMap;public class EventTracker { private final ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>(); public void recordEvent(String eventType) { counts.compute(eventType, (k, v) -> v == null ? 1 : v + 1); } public int getCount(String eventType) { return counts.getOrDefault(eventType, 0); }}Use ConcurrentHashMap.compute() or merge() to perform atomic updates without race conditions.
Exercise 2: Explain why using an unbounded LinkedBlocking
Explain why using an unbounded LinkedBlockingQueue in an HTTP request executor pool can cause an OutOfMemoryError during database outages.
Explore a worked solution
// In an unbounded LinkedBlockingQueue, incoming HTTP requests accumulate indefinitely// when downstream DB transactions stall. Because queue.offer() never rejects tasks,// memory grows linearly until heap exhaustion triggers an OutOfMemoryError.// Fix: Use an ArrayBlockingQueue or LinkedBlockingQueue with an explicit capacity limit// and configure a ThreadPoolExecutor.CallerRunsPolicy or AbortPolicy to enforce backpressure.Think about what happens to queue size when consumer threads are blocked but producer threads keep adding items.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Implement an idempotent event counter using C
Implement an idempotent event counter using ConcurrentHashMap and compute(). If the event exists, increment its count; otherwise set it to 1.
Build successful. (Java 15) [Process exited normally]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.