Concurrent Collections in Java: A Practical Guide.

In Java, handling concurrent programming—where multiple threads access and modify shared data simultaneously—can be challenging. One of the key concepts for managing such scenarios is the use of concurrent collections. Java provides a set of thread-safe collections in the java.util.concurrent package, which are designed to work efficiently in multithreaded environments.

In this guide, we'll explore the various concurrent collections in Java, their use cases, and practical examples of how they can be leveraged to build robust multithreaded applications.

1. Introduction to Concurrent Collections

In a multithreaded program, the need to safely modify and access shared data is crucial. The Java Collections Framework offers several classes designed specifically for handling concurrent modifications and ensuring thread-safety. These collections are implemented to support atomic operations, meaning that a thread can safely perform operations on them without interference from other threads, reducing the chances of data inconsistency or corruption.


2. Why Use Concurrent Collections?

Without thread-safe collections, concurrent access to shared data structures can lead to problems like race conditions, deadlocks, and inconsistent states. For example, if multiple threads are trying to update a map at the same time without synchronization, the map's integrity may be compromised.

Java’s concurrent collections solve these issues by providing built-in synchronization mechanisms, reducing the need for external synchronization like synchronized blocks or locks. These collections are optimized for concurrent access, providing better performance in multithreaded applications compared to synchronized versions of standard collections.


3. Types of Concurrent Collections

CopyOnWriteArrayList

  • Description: CopyOnWriteArrayList is a thread-safe variant of ArrayList. It works by creating a new copy of the underlying array every time an element is added or removed, which ensures that read operations (like get()) are not blocked or synchronized.

  • Use Case: Best suited for scenarios where reads are frequent, and writes (add, remove, update) are rare.

Example:

import java.util.concurrent.CopyOnWriteArrayList;

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

        // Adding elements
        list.add("Java");
        list.add("Concurrency");

        // Reading elements (thread-safe)
        for (String item : list) {
            System.out.println(item);
        }

        // Modifying the list
        list.add("Thread Safety");
        list.remove("Java");
    }
}

ConcurrentHashMap

  • Description: ConcurrentHashMap is a thread-safe variant of HashMap, designed for high concurrency. It partitions the map into segments and allows multiple threads to read or write to different segments concurrently without locking the entire map.

  • Use Case: Best for scenarios that involve frequent reads and updates of key-value pairs, such as caching.

Example:

import java.util.concurrent.ConcurrentHashMap;

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

        // Adding key-value pairs
        map.put("apple", 10);
        map.put("banana", 20);

        // Retrieving values
        System.out.println(map.get("apple"));

        // Updating values
        map.putIfAbsent("orange", 30);
        map.replace("banana", 25);

        // Concurrent updates
        map.compute("apple", (key, value) -> value + 5);
        System.out.println(map.get("apple"));
    }
}

BlockingQueue

  • Description: BlockingQueue is a type of queue that supports operations that block when the queue is full or empty. It is used primarily in producer-consumer scenarios where one or more threads are producing data, and one or more threads are consuming it.

  • Use Case: Ideal for thread-safe queues in multithreaded applications.

Example:

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

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

        // Producer thread
        Thread producer = new Thread(() -> {
            try {
                queue.put("Data 1");
                queue.put("Data 2");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Consumer thread
        Thread consumer = new Thread(() -> {
            try {
                System.out.println(queue.take());
                System.out.println(queue.take());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();
    }
}

ConcurrentLinkedQueue

  • Description: ConcurrentLinkedQueue is a lock-free, thread-safe queue designed for high throughput in multi-threaded environments. It is based on a non-blocking algorithm and allows concurrent insertions and removals.

  • Use Case: Ideal for queues with high concurrent access, where the order of processing matters.

Example:

import java.util.concurrent.ConcurrentLinkedQueue;

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

        // Enqueueing items
        queue.add("Java");
        queue.add("Concurrency");

        // Dequeueing items
        System.out.println(queue.poll());
        System.out.println(queue.poll());
    }
}

ConcurrentSkipListMap

  • Description: ConcurrentSkipListMap is a thread-safe NavigableMap implementation that is based on a skip list. It supports concurrent access and maintains order, which makes it suitable for applications requiring sorted key-value pairs.

  • Use Case: Best suited for scenarios where you need a sorted map with concurrent access.

Example:

import java.util.concurrent.ConcurrentSkipListMap;

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

        // Adding key-value pairs
        map.put(1, "One");
        map.put(3, "Three");

        // Accessing values
        System.out.println(map.firstKey());
        System.out.println(map.lastKey());

        // Iterating over sorted keys
        map.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}

ConcurrentLinkedDeque

  • Description: ConcurrentLinkedDeque is a thread-safe, non-blocking, doubly-linked deque (double-ended queue). It supports efficient insertion and removal from both ends of the queue.

  • Use Case: Useful for applications needing a thread-safe deque where elements can be added or removed from both ends.

Example:

import java.util.concurrent.ConcurrentLinkedDeque;

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

        // Adding elements
        deque.addFirst("First");
        deque.addLast("Last");

        // Removing elements
        System.out.println(deque.removeFirst());
        System.out.println(deque.removeLast());
    }
}

4. Working with Concurrent Collections

Basic Operations

Concurrent collections generally provide the same set of methods as their non-concurrent counterparts, such as add(), remove(), get(), and so on. However, these operations are designed to ensure thread safety. Additionally, many concurrent collections implement atomic methods for specific use cases, such as putIfAbsent() in ConcurrentHashMap or poll() in BlockingQueue.

Thread-Safety and Locking Mechanisms

While concurrent collections are thread-safe, they don't always rely on traditional locking mechanisms (like synchronized). Instead, many collections use advanced techniques like lock-striping (in ConcurrentHashMap) or CAS (Compare-And-Swap) operations to ensure thread safety while maintaining high performance.


5. When to Use Concurrent Collections

  • High Concurrency: When you need multiple threads to interact with the same collection.

  • Efficient Data Access: For scenarios where threads are constantly adding, removing, or updating data concurrently.

  • Producer-Consumer Patterns: In cases where producers and consumers are interacting with a shared collection.


6. Best Practices

  • Use ConcurrentHashMap for key-value stores in highly concurrent environments.

  • Choose BlockingQueue for producer-consumer scenarios where threads must wait for data or space to perform their operations.

  • Use CopyOnWriteArrayList when there are frequent reads and rare writes.


7. Conclusion

Concurrent collections in Java offer a high level of thread-safety and performance for multi-threaded applications. By choosing the appropriate concurrent collection based on your specific requirements, you can significantly reduce the complexity of managing shared resources and ensure that your application performs well under concurrent load.

More such articles:

https://medium.com/techwasti

https://www.youtube.com/@maheshwarligade

https://techwasti.com/series/spring-boot-tutorials

https://techwasti.com/series/go-language

0
Subscribe to my newsletter

Read articles from Maheshwar Ligade directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Maheshwar Ligade
Maheshwar Ligade

Learner, Love to make things simple, Full Stack Developer, StackOverflower, Passionate about using machine learning, deep learning and AI