Concurrent Collections
Use ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, and other thread-safe collections.
Overview
Key Points
- •ConcurrentHashMap allows concurrent reads and partial writes without a global lock
- •ConcurrentHashMap.computeIfAbsent(key, fn) atomically inserts if missing
- •ConcurrentHashMap does not allow null keys or null values
- •CopyOnWriteArrayList creates a new array copy on every write; safe for infrequent writes and frequent iteration
- •BlockingQueue.put() blocks when the queue is full; take() blocks when empty
- •LinkedBlockingQueue has an optional capacity; ArrayBlockingQueue always has a fixed capacity
- •ConcurrentLinkedQueue is non-blocking and unbounded, using CAS internally
- •Collections.synchronizedMap wraps any map but locks the entire map per operation; avoid for high-concurrency
- •ConcurrentHashMap.merge(key, value, remappingFn) is atomic for frequency-map patterns
Examples
ConcurrentHashMap for concurrent word frequency counting
import java.util.*;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
ConcurrentHashMap<String, Integer> freq = new ConcurrentHashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++)
freq.merge(words[i], 1, Integer::sum);
});
Thread t2 = new Thread(() -> {
for (int i = 3; i < 6; i++)
freq.merge(words[i], 1, Integer::sum);
});
t1.start(); t2.start();
t1.join(); t2.join();
freq.forEach((word, count) ->
System.out.println(word + ": " + count));
}
}merge(key, value, fn) is atomic: if the key is absent, it inserts value; if present, it applies fn to (oldValue, value). This avoids a manual get-then-put race.
ArrayBlockingQueue as a producer-consumer channel
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
queue.put(i);
System.out.println("Produced: " + i);
}
queue.put(-1); // poison pill
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
while (true) {
int item = queue.take();
if (item == -1) break;
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}The poison pill pattern (-1) signals the consumer to stop. BlockingQueue handles the synchronization automatically; no explicit locking needed. put() blocks if the queue is full, take() blocks if empty.
CopyOnWriteArrayList for event-listener registries
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
public class EventBus {
private final CopyOnWriteArrayList<Consumer<String>> listeners =
new CopyOnWriteArrayList<>();
public void subscribe(Consumer<String> listener) {
listeners.add(listener);
}
public void publish(String event) {
// Iterating CopyOnWriteArrayList is safe even if listeners are added concurrently
for (Consumer<String> listener : listeners) {
listener.accept(event);
}
}
public static void main(String[] args) {
EventBus bus = new EventBus();
bus.subscribe(e -> System.out.println("Listener A: " + e));
bus.subscribe(e -> System.out.println("Listener B: " + e));
bus.publish("click");
bus.publish("submit");
}
}CopyOnWriteArrayList shines when iteration is far more frequent than mutation. Adding a listener is O(n) because the entire array is copied, but iteration never throws ConcurrentModificationException.
Common Mistakes
- Using Collections.synchronizedMap in performance-critical concurrent code. It locks the entire map on every get/put, serializing all threads. Use ConcurrentHashMap instead.
- Null keys or values in ConcurrentHashMap. Unlike HashMap, ConcurrentHashMap throws NullPointerException for null keys or values. Use a sentinel value (like an empty string) if you need to represent absent.
- Iterating a synchronized wrapper without holding the lock. Even with Collections.synchronizedMap, iteration is not thread-safe unless you synchronize the entire iteration block. ConcurrentHashMap iterators are weakly consistent and do not throw.
- Treating CopyOnWriteArrayList as efficient for write-heavy workloads. Every add or remove copies the entire backing array. Use only when reads vastly outnumber writes.
Note
For thread-safe lazy initialization of a ConcurrentHashMap entry, use computeIfAbsent(key, k -> new ArrayList<>()) rather than a manual get-then-put pattern. computeIfAbsent is atomic and avoids constructing the value object unnecessarily if the key already exists.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4