Use ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue and other thread-safe collections.
Published February 15, 2025
The java.util.concurrent package provides thread-safe collection implementations that outperform manually synchronized collections by using fine-grained locking or lock-free algorithms.
The go-to thread-safe map. Uses segment-based locking (Java 7) or CAS + synchronized per bucket (Java 8+), allowing concurrent reads and fine-grained writes.
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// All operations are thread-safe
map.put("a", 1);
map.get("a"); // never returns stale data
map.remove("a");
// Atomic compound operations
map.putIfAbsent("b", 2); // put only if key absent
map.computeIfAbsent("c", k -> k.length()); // compute if absent
map.computeIfPresent("c", (k, v) -> v + 1); // update if present
map.compute("d", (k, v) -> v == null ? 1 : v + 1); // always compute
map.merge("e", 1, Integer::sum); // merge with existing value
// ConcurrentHashMap does NOT allow null keys or null values
map.put(null, 1); // throws NullPointerException!
Thread-safe list that creates a fresh copy of the array on every write. Perfect for read-heavy, write-rare scenarios (event listener lists, configuration).
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// Iteration is safe even while another thread is adding/removing
for (String item : list) {
// snapshot iteration — won't throw ConcurrentModificationException
process(item);
}
list.add("item"); // creates a new copy of the internal array
// Warning: very expensive for write-heavy workloads!
A thread-safe queue that blocks on take() (if empty) and put() (if full). Perfect for producer-consumer patterns.
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100); // bounded
// Producer
new Thread(() -> {
while (true) {
queue.put(generateTask()); // blocks if queue full
}
}).start();
// Consumer
new Thread(() -> {
while (true) {
Task task = queue.take(); // blocks if queue empty
process(task);
}
}).start();
BlockingQueue implementations:
LinkedBlockingQueue — optionally bounded, linked nodesArrayBlockingQueue — fixed-capacity, array-backed, FIFOPriorityBlockingQueue — unbounded, ordered by prioritySynchronousQueue — zero-capacity; each put blocks until a takeDelayQueue — elements become available only after a delay// Non-blocking, lock-free FIFO queue (uses CAS)
Queue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("item"); // non-blocking add
queue.poll(); // non-blocking remove (returns null if empty)
queue.peek(); // non-blocking peek
// These synchronize every method but not iteration
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// UNSAFE: iteration is not synchronized!
for (String s : syncList) { ... } // must wrap in synchronized(syncList) {}
// Prefer CopyOnWriteArrayList or ConcurrentHashMap instead
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic ++
counter.getAndAdd(5); // atomic +=5
counter.compareAndSet(5, 10); // CAS: if current==5, set to 10
AtomicReference<String> ref = new AtomicReference<>("initial");
ref.compareAndSet("initial", "updated"); // atomic reference swap
AtomicLong longCounter = new AtomicLong();
LongAdder adder = new LongAdder(); // better for high-contention counting
ConcurrentHashMap vs Hashtable: CHM uses fine-grained locks; Hashtable synchronizes every method with a single lock — CHM is dramatically faster.ConcurrentHashMap vs Collections.synchronizedMap: same issue — synchronized map holds one lock for all operations.LongAdder over AtomicLong: under high contention, LongAdder distributes counting across multiple cells, avoiding CAS retry loops.