Skip to content
Dev Dump

Concurrent Collections

Standard collections (ArrayList, HashMap, etc.) are not thread-safe. If multiple threads read and write to them simultaneously, you get corrupted data or ConcurrentModificationException. Java provides two approaches to solve this.

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());

These wrap every method call in a synchronized block. Simple, but slow — only one thread can access the collection at a time, even for reads.

Critical: Iteration is still not safe — you must manually synchronize:

synchronized (syncList) {
for (String item : syncList) {
process(item);
}
}

Purpose-built for concurrency. Better performance because they use fine-grained locking, lock-free algorithms, or copy-on-write strategies instead of locking the entire collection.

The go-to thread-safe Map. Uses segment-level locking (Java 7) / CAS + synchronized on individual buckets (Java 8+), so multiple threads can read and write simultaneously to different parts of the map.

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("Alice", 95);
map.get("Alice");
map.putIfAbsent("Bob", 87);
map.remove("Alice");
// Atomic compound operations — these are why you use ConcurrentHashMap
map.compute("Alice", (key, val) -> (val == null) ? 1 : val + 1);
map.merge("Alice", 1, Integer::sum);

Key differences from HashMap:

  • No null keys or values allowed
  • size() is an estimate under concurrency — use mappingCount() for a long-based count
  • Iterators are fail-safe (weakly consistent)
  • Bulk operations: forEach, search, reduce with parallelism threshold
// Parallel forEach — uses ForkJoinPool when entries exceed threshold
map.forEach(2, (key, val) -> System.out.println(key + "=" + val));
// Search in parallel — stops at first match
String found = map.search(2, (key, val) -> val > 90 ? key : null);
// Reduce in parallel
int total = map.reduce(2, (key, val) -> val, Integer::sum);

A thread-safe List optimized for read-heavy workloads. Every write (add, set, remove) creates a new copy of the internal array. Reads never block.

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
list.get(0); // lock-free, very fast
// Iteration is always on a snapshot — safe even if another thread modifies the list
for (String item : list) {
// never throws ConcurrentModificationException
}

Trade-offs:

  • Reads: O(1), no locking — very fast
  • Writes: O(n) — copies the entire array every time
  • Best for: listener lists, configuration lists, small collections that are read far more than written

Avoid when writes are frequent or the list is large.

Same concept as CopyOnWriteArrayList, but for Set. Backed by a CopyOnWriteArrayList internally, so contains() is O(n), not O(1).

CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
set.add("listener1");
set.add("listener2");

Only use for small sets with rare modifications (e.g., event listeners).

Designed for producer-consumer patterns. Threads block (wait) when trying to take from an empty queue or put into a full queue.

BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); // capacity 10
// Producer thread
queue.put("task"); // blocks if queue is full
// Consumer thread
String task = queue.take(); // blocks if queue is empty
ThrowsReturns special valueBlocksTimes out
Insertadd(e)offer(e)put(e)offer(e, time, unit)
Removeremove()poll()take()poll(time, unit)
Examineelement()peek()
ImplementationBounded?Best For
ArrayBlockingQueueYes (fixed)Bounded producer-consumer
LinkedBlockingQueueOptionalHigh-throughput producer-consumer
PriorityBlockingQueueNoPriority-based task processing
SynchronousQueueZero capacityDirect handoff between threads
DelayQueueNoScheduled/delayed task execution
// Bounded queue — producers wait when full
BlockingQueue<Task> taskQueue = new ArrayBlockingQueue<>(100);
// Unbounded queue — producers never block (watch for OOM)
BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>();
// Priority queue — consumers get highest-priority item
BlockingQueue<Task> taskQueue = new PriorityBlockingQueue<>();

ConcurrentLinkedQueue / ConcurrentLinkedDeque

Section titled “ConcurrentLinkedQueue / ConcurrentLinkedDeque”

Lock-free, non-blocking concurrent queue/deque using CAS operations. Unlike BlockingQueue, these never block — they return null or false when empty/full.

ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("item");
String item = queue.poll(); // null if empty (never blocks)

Use when you need a concurrent queue but don’t want blocking behavior.

ConcurrentSkipListMap / ConcurrentSkipListSet

Section titled “ConcurrentSkipListMap / ConcurrentSkipListSet”

Thread-safe sorted collections. Like TreeMap/TreeSet but concurrent.

ConcurrentSkipListMap<String, Integer> sortedMap = new ConcurrentSkipListMap<>();
sortedMap.put("banana", 2);
sortedMap.put("apple", 1);
// Iteration in sorted key order, thread-safe
NeedUse
Thread-safe MapConcurrentHashMap
Thread-safe sorted MapConcurrentSkipListMap
Thread-safe List (read-heavy)CopyOnWriteArrayList
Thread-safe List (write-heavy)Collections.synchronizedList() or redesign
Producer-consumer queueArrayBlockingQueue or LinkedBlockingQueue
Non-blocking concurrent queueConcurrentLinkedQueue
Thread-safe SetCopyOnWriteArraySet (small) or ConcurrentHashMap.newKeySet()

Synchronized Wrappers vs Concurrent Collections

Section titled “Synchronized Wrappers vs Concurrent Collections”
Synchronized WrappersConcurrent Collections
LockingEntire collectionFine-grained / lock-free
Read performanceBlocked by writersReads often lock-free
IterationMust sync manuallyFail-safe (snapshot)
Compound opsNot atomicAtomic (putIfAbsent, compute)
When to useQuick fix, low contentionProduction, high concurrency

Bottom line: Always prefer java.util.concurrent collections over synchronized wrappers in new code.