Home/Learn/Java A–Z/Concurrent Collections

Concurrent Collections

Intermediate
Concurrency

java.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.

ConcurrentHashMap.java
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).

BlockingQueue.java
// 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).

OtherCollections.java
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

NEWRUNNABLERUNNINGBLOCKEDWAITINGTERMINATED
synchronized(lock)— free
main
RUNNING
t1
NEW
t2
NEW
main thread creates Thread t1 and Thread t2. Both are in NEW state.
1 / 6

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 Aria
1

How is ConcurrentHashMap different from Collections.synchronizedMap()?

MediumAmazon
2

What is the difference between put() and offer() in a BlockingQueue?

EasyGoogle
3

When would you use CopyOnWriteArrayList?

MediumOracle
4

What is a poison pill pattern and how does it work with BlockingQueue?

MediumNetflix
5

What are the atomic operations available on ConcurrentHashMap?

MediumMicrosoft

Ask Aria about Concurrent Collections

Your personal AI tutor — ask anything about this concept