Concurrent Collections in Java
Introduction
In a multi-threaded Java application, sharing data across threads is a common necessity. However, using standard collections like ArrayList or HashMap in a concurrent environment leads to disastrous results: data corruption, ConcurrentModificationException, and unpredictable race conditions.
To solve this, Java provides the java.util.concurrent package, featuring highly optimized Concurrent Collections. These collections are designed specifically for thread safety and high throughput, circumventing the severe performance bottlenecks of older, fully synchronized collections like Vector or Hashtable.
This guide explores the most important concurrent collections, how they work under the hood, and when you should use them.
1. The Problem with Synchronization
Before concurrent collections, developers used Collections.synchronizedList() or Hashtable. These wrappers achieve thread safety by synchronizing every single method on the collection's monitor lock.
The Bottleneck: When Thread A is reading from a synchronized map, Thread B cannot read or write to it. It must wait. In highly concurrent applications, this creates massive contention, bringing performance to a crawl.
Concurrent collections solve this by utilizing advanced lock-free algorithms, atomic variables (CAS operations), and fine-grained locking strategies (like Lock Striping).
2. ConcurrentHashMap
ConcurrentHashMap is arguably the most frequently used concurrent collection. It provides the full functionality of a HashMap but is entirely thread-safe and highly performant.
How it Works
- Java 7 and earlier (Lock Striping): The map was divided into segments (default 16). Each segment had its own lock. This meant up to 16 threads could write to the map simultaneously, provided they were writing to different segments. Read operations rarely required locks.
- Java 8 and later (CAS and Synchronized Node): Segments were removed. It now uses a combination of Compare-And-Swap (CAS) operations and synchronizing only the specific "bucket" (Node) being modified. This provides even finer-grained locking and greater concurrency.
Usage Example
ConcurrentMap<String, Integer> cache = new ConcurrentHashMap<>();
// Thread-safe put if absent
cache.putIfAbsent("users_count", 0);
// Atomic computation
cache.computeIfPresent("users_count", (key, val) -> val + 1);
3. CopyOnWriteArrayList and CopyOnWriteArraySet
CopyOnWriteArrayList is a thread-safe variant of ArrayList.
How it Works
As the name implies, every time the list is modified (add, set, remove), a fresh copy of the underlying array is created. The modification is made to the new copy, and then the reference is atomically swapped.
- Reads are lock-free: Because reads happen on an immutable array, multiple threads can iterate over the list simultaneously without locks and without ever throwing
ConcurrentModificationException. - Writes are expensive: Creating a new array on every write is memory and CPU intensive.
When to use it
Use CopyOnWriteArrayList when you have an overwhelming majority of read operations and very few write operations (e.g., maintaining a list of event listeners or a cache of rarely changing configurations).
List<String> subscribers = new CopyOnWriteArrayList<>();
subscribers.add("user1@example.com"); // Creates a new array copy
// Iteration is perfectly safe, even if another thread adds an item right now
for(String sub : subscribers) {
System.out.println("Sending email to " + sub);
}
4. Concurrent Queues
Queues are heavily used in producer-consumer architectures. The java.util.concurrent package provides several concurrent queues.
ConcurrentLinkedQueue
An unbounded, thread-safe, non-blocking queue based on linked nodes. It relies on non-blocking CAS operations. Excellent for highly concurrent environments where you do not need blocking behavior (i.e., you don't need a thread to pause and wait for an item to be added to an empty queue).
BlockingQueues
BlockingQueue interfaces wait for the queue to become non-empty when retrieving an element, and wait for space to become available when storing an element. This is the foundation of the Producer-Consumer pattern.
ArrayBlockingQueue: A bounded, array-backed blocking queue. Because it has a fixed capacity, it preventsOutOfMemoryErrorin scenarios where producers are faster than consumers. Uses a single lock for both reading and writing.LinkedBlockingQueue: Optionally bounded, linked-node-backed queue. It uses "Two-Lock Queues" (one lock for putting, one for taking), allowing a producer and a consumer to operate concurrently. Generally offers higher throughput thanArrayBlockingQueue.PriorityBlockingQueue: An unbounded blocking queue that uses the same ordering rules as classPriorityQueueand supplies blocking retrieval operations.
Usage Example (Producer-Consumer)
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer Thread
Runnable producer = () -> {
try {
queue.put("Data Packet"); // Blocks if queue is full
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
};
// Consumer Thread
Runnable consumer = () -> {
try {
String data = queue.take(); // Blocks if queue is empty
System.out.println("Processed: " + data);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
};
5. ConcurrentSkipListMap and ConcurrentSkipListSet
These are thread-safe equivalents to TreeMap and TreeSet. They keep elements sorted according to their natural ordering (or a provided Comparator).
They are implemented using a Skip List data structure rather than a Red-Black Tree. Skip lists use a probabilistic hierarchy of linked lists, allowing for highly efficient, lock-free insertions, deletions, and searches, averaging O(log n) time.
Use these when you need a sorted map/set that supports high concurrency.
Summary
Java's concurrent collections provide a robust toolkit for multithreaded programming:
- Use
ConcurrentHashMapfor thread-safe key-value storage with high throughput. - Use
CopyOnWriteArrayListfor lists with heavy read and minimal write operations. - Use
BlockingQueueimplementations for robust producer-consumer message passing. - Use
ConcurrentSkipListMapwhen you need concurrent, sorted data structures.
Choosing the right concurrent collection prevents performance bottlenecks, ensures data integrity, and significantly reduces the complexity of writing safe concurrent code.