Java - Java Concurrent Collections

Java Concurrent Collections are a group of thread-safe collection classes provided mainly through the java.util.concurrent package. They are designed for programs in which multiple threads need to access and modify collections at the same time. Traditional collections such as ArrayList, HashMap, and HashSet are not inherently safe for concurrent modification, so using them directly from multiple threads can result in inconsistent data or unexpected behavior. Concurrent collections provide mechanisms that allow multiple threads to work with shared data more safely and efficiently.

Why Concurrent Collections Are Needed

In a multithreaded application, several threads may attempt to read, add, remove, or update elements in the same collection simultaneously. For example, imagine a web application where many users are accessing a shared collection of active sessions. If several threads modify an ordinary HashMap at the same time, the collection can enter an inconsistent state.

One traditional solution is to synchronize access to the entire collection. For example, a developer could use Collections.synchronizedMap(). However, synchronizing an entire collection can reduce performance because only one thread may be able to perform certain operations at a time. Concurrent collections are designed to provide better scalability by allowing multiple threads to operate concurrently where possible.

Important Concurrent Collection Classes

Some of the most commonly used concurrent collections are:

  • ConcurrentHashMap

  • CopyOnWriteArrayList

  • CopyOnWriteArraySet

  • ConcurrentLinkedQueue

  • ConcurrentLinkedDeque

  • BlockingQueue

  • ConcurrentSkipListMap

  • ConcurrentSkipListSet

Each class is designed for a particular type of concurrent programming requirement.

ConcurrentHashMap

ConcurrentHashMap is a thread-safe implementation of the Map interface. It allows multiple threads to read and update the map concurrently without requiring the entire map to be locked for every operation.

import java.util.concurrent.ConcurrentHashMap;

public class Example {
    public static void main(String[] args) {
        ConcurrentHashMap<Integer, String> students =
                new ConcurrentHashMap<>();

        students.put(1, "Rahul");
        students.put(2, "Anita");
        students.put(3, "Kiran");

        System.out.println(students.get(2));
    }
}

One important advantage of ConcurrentHashMap is that it generally provides better concurrency than synchronizing a normal HashMap. Multiple threads can perform operations on the map without unnecessary blocking.

ConcurrentHashMap does not allow null keys or null values. It also provides useful atomic methods such as putIfAbsent(), computeIfAbsent(), compute(), merge(), and replace().

For example:

ConcurrentHashMap<String, Integer> scores =
        new ConcurrentHashMap<>();

scores.putIfAbsent("Amit", 80);
scores.computeIfAbsent("Priya", key -> 90);

System.out.println(scores);

These methods are particularly useful when several threads may try to update the same data.

CopyOnWriteArrayList

CopyOnWriteArrayList is a thread-safe implementation of the List interface. It is especially useful when a collection is read frequently but modified relatively rarely.

When an element is added, removed, or changed, the underlying array is copied. Existing readers can continue working with the previous version while the modification takes place.

import java.util.concurrent.CopyOnWriteArrayList;

public class Example {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> names =
                new CopyOnWriteArrayList<>();

        names.add("Anita");
        names.add("Rahul");
        names.add("Kiran");

        for (String name : names) {
            System.out.println(name);
        }
    }
}

This approach makes iteration safe even when another thread modifies the collection. However, copying the array whenever the list changes can be expensive when there are frequent modifications or when the collection is very large.

Therefore, CopyOnWriteArrayList is most appropriate for read-heavy applications.

CopyOnWriteArraySet

CopyOnWriteArraySet is a thread-safe set based on the copy-on-write approach. It does not allow duplicate elements.

import java.util.concurrent.CopyOnWriteArraySet;

public class Example {
    public static void main(String[] args) {
        CopyOnWriteArraySet<String> cities =
                new CopyOnWriteArraySet<>();

        cities.add("Bengaluru");
        cities.add("Mysuru");
        cities.add("Bengaluru");

        System.out.println(cities);
    }
}

The second attempt to add "Bengaluru" does not create a duplicate.

Like CopyOnWriteArrayList, this collection is useful when reads are much more common than modifications.

ConcurrentLinkedQueue

ConcurrentLinkedQueue is a thread-safe, non-blocking queue. It is suitable when multiple threads need to add and remove elements from a queue without using a traditional lock-based approach.

import java.util.concurrent.ConcurrentLinkedQueue;

public class Example {
    public static void main(String[] args) {
        ConcurrentLinkedQueue<String> tasks =
                new ConcurrentLinkedQueue<>();

        tasks.offer("Task 1");
        tasks.offer("Task 2");
        tasks.offer("Task 3");

        System.out.println(tasks.poll());
        System.out.println(tasks);
    }
}

offer() adds an element to the queue, while poll() retrieves and removes the head element. peek() retrieves the head without removing it.

This type of queue can be useful in producer-consumer systems and other applications where multiple threads need to exchange tasks.

BlockingQueue

BlockingQueue is particularly useful for producer-consumer applications. Unlike a normal queue, a blocking queue can make a thread wait when the queue is empty or full, depending on the operation being performed.

Common implementations include:

  • ArrayBlockingQueue

  • LinkedBlockingQueue

  • PriorityBlockingQueue

  • DelayQueue

Example:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Example {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue =
                new ArrayBlockingQueue<>(2);

        queue.put("Task 1");
        queue.put("Task 2");

        System.out.println(queue.take());
    }
}

The put() method waits if the queue has reached its capacity, while take() waits if the queue is empty.

This behavior makes BlockingQueue very useful for safely coordinating producer and consumer threads.

ConcurrentLinkedDeque

ConcurrentLinkedDeque is a thread-safe double-ended queue. It allows elements to be inserted and removed from both ends.

import java.util.concurrent.ConcurrentLinkedDeque;

public class Example {
    public static void main(String[] args) {
        ConcurrentLinkedDeque<String> deque =
                new ConcurrentLinkedDeque<>();

        deque.addFirst("A");
        deque.addLast("B");

        System.out.println(deque.removeFirst());
        System.out.println(deque.removeLast());
    }
}

It is useful when a concurrent application needs queue and stack-like behavior from both ends.

ConcurrentSkipListMap and ConcurrentSkipListSet

ConcurrentSkipListMap is a thread-safe sorted map. It maintains its keys in sorted order and supports concurrent access.

import java.util.concurrent.ConcurrentSkipListMap;

public class Example {
    public static void main(String[] args) {
        ConcurrentSkipListMap<Integer, String> employees =
                new ConcurrentSkipListMap<>();

        employees.put(103, "Kiran");
        employees.put(101, "Anita");
        employees.put(102, "Rahul");

        System.out.println(employees);
    }
}

The keys are maintained in sorted order.

ConcurrentSkipListSet provides similar functionality for sets. It maintains elements in sorted order while supporting concurrent operations.

Atomic Operations

One important feature of concurrent collections is support for operations that must happen as a single logical action.

Consider this code:

if (!map.containsKey("Java")) {
    map.put("Java", 1);
}

In a multithreaded environment, another thread could modify the map between containsKey() and put(). This creates a race condition.

A concurrent map provides atomic alternatives:

map.putIfAbsent("Java", 1);

Here, checking whether the key exists and inserting the value are treated as one atomic operation.

Similarly, methods such as computeIfAbsent() and merge() can simplify thread-safe updates.

Iteration in Concurrent Collections

Concurrent collections generally provide special iteration behavior. For example, iterators of ConcurrentHashMap are designed to work safely while other threads modify the map. They do not generally throw ConcurrentModificationException merely because another thread modifies the collection during iteration.

However, this does not mean that an iterator represents a perfectly frozen snapshot of the collection. Changes made by other threads may or may not be visible during the iteration, depending on the collection and operation.

Therefore, developers should understand the consistency guarantees of the specific concurrent collection they are using.

Concurrent Collections vs Synchronized Collections

There is an important difference between concurrent collections and synchronized collections.

A synchronized collection generally protects operations by using synchronization around the collection. This can create significant contention when many threads access the same collection.

Concurrent collections are specifically designed for multithreaded environments. They often use techniques such as fine-grained synchronization, lock-free algorithms, or other concurrency mechanisms to allow greater parallelism.

For example:

Map<Integer, String> map =
    Collections.synchronizedMap(new HashMap<>());

This is thread-safe, but access may involve broader synchronization.

In contrast:

ConcurrentHashMap<Integer, String> map =
    new ConcurrentHashMap<>();

is specifically designed for concurrent access and typically provides better scalability for many concurrent workloads.

Choosing the Appropriate Concurrent Collection

The correct collection depends on the application's requirements.

Use ConcurrentHashMap when multiple threads need to access and modify key-value data.

Use CopyOnWriteArrayList when there are many more reads than writes.

Use CopyOnWriteArraySet when a thread-safe set is required and modifications are relatively infrequent.

Use ConcurrentLinkedQueue when multiple threads need a non-blocking queue.

Use BlockingQueue when producers and consumers need to coordinate through a queue.

Use ConcurrentLinkedDeque when concurrent operations are required at both ends of a deque.

Use ConcurrentSkipListMap or ConcurrentSkipListSet when thread safety and sorted ordering are both required.

Advantages of Concurrent Collections

Concurrent collections provide several advantages in multithreaded Java applications. They reduce the need for developers to manually synchronize every collection operation, provide safer access to shared data, and can offer better performance than broadly synchronized collections under high concurrency.

They are particularly useful in web servers, task-processing systems, caching systems, messaging applications, background-processing systems, and other applications where many threads interact with shared data.

Limitations

Concurrent collections do not automatically make an entire application thread-safe. While individual collection operations may be thread-safe, a sequence of multiple operations may still require additional synchronization or atomic logic.

For example:

if (!map.containsKey(key)) {
    map.put(key, value);
}

The two operations together are not necessarily atomic simply because the map is a ConcurrentHashMap. In such situations, methods such as putIfAbsent() or computeIfAbsent() should be considered.

Another limitation is that some concurrent collections have specific performance trade-offs. CopyOnWriteArrayList, for example, can become inefficient when modifications are frequent because each modification involves copying the underlying array.

Conclusion

Java Concurrent Collections provide specialized data structures for safely handling shared collections in multithreaded programs. Classes such as ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue, and BlockingQueue make it easier to develop applications where multiple threads work with common data.

The key to using them effectively is to select the collection according to the application's access pattern. Concurrent collections improve thread safety and scalability, but developers must still understand atomicity, visibility, iteration behavior, and the difference between individual thread-safe operations and thread-safe sequences of operations.