Concurrent Collections
Intermediatejava.util.concurrent provides thread-safe collections optimised for concurrent access — far more scalable than synchronized wrappers.
Overview
Wrapping a collection with Collections.synchronizedMap() or synchronizedList() serialises all access — one thread at a time. The java.util.concurrent package provides purpose-built thread-safe collections: ConcurrentHashMap (segmented locking), CopyOnWriteArrayList (lock-free reads), BlockingQueue implementations (ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue), and ConcurrentLinkedQueue. Each is optimised for specific access patterns.
ConcurrentHashMap
ConcurrentHashMap is the concurrent replacement for HashMap and Collections.synchronizedMap(). It uses fine-grained locking (per-bucket in Java 8+) to allow high concurrency. Multiple threads can read and write simultaneously as long as they hit different buckets.
Atomic operations: putIfAbsent(), computeIfAbsent(), compute(), merge() — all atomic and very useful.
import java.util.concurrent.*;
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Thread-safe put/get/remove
map.put("a", 1);
map.get("a");
// Atomic operations — essential for concurrent use
// Only inserts if key absent
map.putIfAbsent("a", 2); // no-op, already exists
// Compute atomically
map.computeIfAbsent("b", k -> k.length()); // b → 1
map.computeIfPresent("b", (k, v) -> v * 2); // b → 2
// Atomic increment (word frequency count)
map.merge("word", 1, Integer::sum);
map.merge("word", 1, Integer::sum); // "word" → 2
// compute — always called (present or absent)
map.compute("counter", (k, v) -> v == null ? 1 : v + 1);
// Bulk operations (Java 8+) — parallel-friendly
map.forEach(2, // parallelism threshold
(k, v) -> System.out.println(k + "=" + v));
int total = map.reduceValues(1, Integer::sum);BlockingQueue
BlockingQueue is the producer-consumer backbone. put() blocks if the queue is full; take() blocks if the queue is empty. This provides back-pressure naturally.
ArrayBlockingQueue: bounded, backed by array. LinkedBlockingQueue: optionally bounded, backed by linked nodes. PriorityBlockingQueue: unbounded, ordered. SynchronousQueue: zero capacity — each put must wait for a take (direct handoff).
// Producer-Consumer with BlockingQueue
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
Thread producer = Thread.ofVirtual().start(() -> {
try {
for (int i = 0; i < 1000; i++) {
queue.put("Task-" + i); // blocks if full
}
queue.put("DONE"); // poison pill
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Consumer
Thread consumer = Thread.ofVirtual().start(() -> {
try {
while (true) {
String task = queue.take(); // blocks if empty
if ("DONE".equals(task)) break;
process(task);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.join();
consumer.join();CopyOnWriteArrayList and Other Collections
CopyOnWriteArrayList creates a fresh copy of the underlying array on every write. This makes reads completely lock-free — ideal for read-heavy lists with infrequent writes (event listener lists, config snapshots).
ConcurrentLinkedQueue: lock-free, non-blocking FIFO queue using CAS. ConcurrentSkipListMap/Set: thread-safe sorted map/set (like ConcurrentTreeMap).
import java.util.concurrent.*;
// CopyOnWriteArrayList — ideal for listener lists
CopyOnWriteArrayList<EventListener> listeners =
new CopyOnWriteArrayList<>();
// Safe iteration — snapshot of array at time of iteration
for (EventListener l : listeners) {
l.onEvent(event); // no ConcurrentModificationException
}
// Writes are expensive (full array copy) — fine for small, read-heavy lists
listeners.add(newListener);
listeners.remove(oldListener);
// ConcurrentLinkedQueue — non-blocking FIFO
Queue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("item"); // never blocks
String item = queue.poll(); // null if empty (no blocking)
// ConcurrentSkipListMap — sorted, thread-safe
ConcurrentNavigableMap<Integer, String> sorted =
new ConcurrentSkipListMap<>();
sorted.put(3, "c");
sorted.put(1, "a");
sorted.put(2, "b");
System.out.println(sorted.firstKey()); // 1
System.out.println(sorted.headMap(2)); // {1=a}Interactive Visualization
Key Points to Remember
- ConcurrentHashMap: high-concurrency map with atomic operations (computeIfAbsent, merge).
- Collections.synchronizedMap() serialises all access — prefer ConcurrentHashMap.
- BlockingQueue: put() blocks when full; take() blocks when empty — natural back-pressure.
- CopyOnWriteArrayList: lock-free reads via array snapshot — good for read-heavy listener lists.
- Never use ArrayList, HashMap, or HashSet across threads without synchronisation.
Practice Concurrent Collections in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaHow is ConcurrentHashMap different from Collections.synchronizedMap()?
What is the difference between put() and offer() in a BlockingQueue?
When would you use CopyOnWriteArrayList?
What is a poison pill pattern and how does it work with BlockingQueue?
What are the atomic operations available on ConcurrentHashMap?
Ask Aria about Concurrent Collections
Your personal AI tutor — ask anything about this concept