Concurrent Collections
Standard collections like ArrayList and HashMap are not thread-safe. The java.util.concurrent package provides concurrent alternatives designed for multi-threaded access.
ConcurrentHashMap
Thread-safe hash map with fine-grained locking (since Java 8, uses CAS and synchronized buckets):
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("Alice", 1);
map.putIfAbsent("Bob", 2);
map.compute("Alice", (key, val) -> val == null ? 1 : val + 1);
map.merge("Charlie", 1, Integer::sum);
// Thread-safe iteration — weakly consistent (no ConcurrentModificationException)
map.forEach((k, v) -> System.out.println(k + " = " + v));
Atomic Compound Operations
// ❌ Not atomic — race condition
if (!map.containsKey(key)) {
map.put(key, computeValue());
}
// ✅ Atomic
map.computeIfAbsent(key, k -> computeValue());
CopyOnWriteArrayList
Creates a new copy of the underlying array on every write — ideal for read-heavy, write-rare scenarios:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("Alice");
list.add("Bob");
// Safe iteration even if list is modified during iteration
for (String name : list) {
list.add("Charlie"); // no ConcurrentModificationException
}
Trade-off: Writes are expensive (full array copy). Use when reads vastly outnumber writes (e.g., listener lists).
CopyOnWriteArraySet
Same copy-on-write semantics for a thread-safe set.
BlockingQueue
Supports blocking put and take operations — the foundation of producer-consumer patterns:
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put("task-1"); // blocks if queue is full
// Consumer
String task = queue.take(); // blocks if queue is empty
Implementations
| Class | Bounded | Notes |
|---|---|---|
ArrayBlockingQueue |
Yes | Array-backed, single lock |
LinkedBlockingQueue |
Optional | Linked nodes, two locks |
PriorityBlockingQueue |
No | Priority ordering |
SynchronousQueue |
Zero capacity | Direct handoff |
DelayQueue |
No | Elements available after delay |
Producer-Consumer Example
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(50);
// Producers
executor.submit(() -> {
while (running) {
queue.put(generateTask());
}
});
// Consumers
for (int i = 0; i < 4; i++) {
executor.submit(() -> {
while (running) {
Task task = queue.take();
process(task);
}
});
}
ConcurrentLinkedQueue
Unbounded non-blocking queue using CAS operations:
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("item");
String item = queue.poll();
Higher throughput than BlockingQueue when blocking is not needed.
ConcurrentSkipListMap / ConcurrentSkipListSet
Sorted concurrent map and set:
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(3, "three");
map.put(1, "one");
map.headMap(3); // views are thread-safe
Choosing the Right Collection
| Scenario | Collection |
|---|---|
| Shared cache / map | ConcurrentHashMap |
| Listener list (few writes) | CopyOnWriteArrayList |
| Task queue with backpressure | ArrayBlockingQueue |
| High-throughput handoff | SynchronousQueue |
| Sorted concurrent map | ConcurrentSkipListMap |
| Lock-free queue | ConcurrentLinkedQueue |
Best Practices
- Prefer concurrent collections over wrapping with
Collections.synchronizedMap() - Use atomic methods (
computeIfAbsent,merge) instead of check-then-act - Do not use
ConcurrentHashMapwith null keys or values — they throw NPE - Choose
CopyOnWrite*only for read-dominated workloads