Concurrent collections in Java with examples

Published June 25, 2024

Concurrent collections in Java with examples

Java provides several concurrent collections that are part of the java.util.concurrent package. These collections are designed to handle multithreaded environments efficiently, providing thread-safe operations and improving performance over traditional synchronization techniques. In this article, we will explore these collections, their use cases, and provide more complex code examples to help you understand how to use them effectively.

Introduction to Concurrent Collections

Concurrent collections are designed to support high concurrency while maintaining thread safety. The main concurrent collections provided by Java are:

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • CopyOnWriteArraySet
  • ConcurrentLinkedQueue
  • ConcurrentLinkedDeque
  • LinkedBlockingQueue
  • PriorityBlockingQueue

Let’s dive into each of these collections with more complex examples.

ConcurrentHashMap

ConcurrentHashMap is a thread-safe implementation of HashMap that allows concurrent read and write operations.

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        
        // Simulate multiple threads updating the map
        Runnable writerTask = () -> {
            for (int i = 0; i < 1000; i++) {
                map.put(Thread.currentThread().getName() + i, i);
            }
        };
        
        Thread writer1 = new Thread(writerTask);
        Thread writer2 = new Thread(writerTask);
        
        writer1.start();
        writer2.start();
        
        try {
            writer1.join();
            writer2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // Concurrent read operation
        Runnable readerTask = () -> {
            map.forEach((key, value) -> System.out.println(Thread.currentThread().getName() + ": " + key + ": " + value));
        };
        
        Thread reader1 = new Thread(readerTask);
        Thread reader2 = new Thread(readerTask);
        
        reader1.start();
        reader2.start();
    }
}

CopyOnWriteArrayList

CopyOnWriteArrayList is a thread-safe variant of ArrayList where all mutative operations are implemented by making a fresh copy of the underlying array.

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        
        // Simulate multiple threads updating the list
        Runnable writerTask = () -> {
            for (int i = 0; i < 1000; i++) {
                list.add(Thread.currentThread().getName() + i);
            }
        };
        
        Thread writer1 = new Thread(writerTask);
        Thread writer2 = new Thread(writerTask);
        
        writer1.start();
        writer2.start();
        
        try {
            writer1.join();
            writer2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // Concurrent read operation
        Runnable readerTask = () -> {
            for (String item : list) {
                System.out.println(Thread.currentThread().getName() + ": " + item);
            }
        };
        
        Thread reader1 = new Thread(readerTask);
        Thread reader2 = new Thread(readerTask);
        
        reader1.start();
        reader2.start();
    }
}

CopyOnWriteArraySet

CopyOnWriteArraySet is a thread-safe variant of HashSet that is implemented using a CopyOnWriteArrayList.

import java.util.concurrent.CopyOnWriteArraySet;

public class CopyOnWriteArraySetExample {
    public static void main(String[] args) {
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
        
        // Simulate multiple threads updating the set
        Runnable writerTask = () -> {
            for (int i = 0; i < 1000; i++) {
                set.add(Thread.currentThread().getName() + i);
            }
        };
        
        Thread writer1 = new Thread(writerTask);
        Thread writer2 = new Thread(writerTask);
        
        writer1.start();
        writer2.start();
        
        try {
            writer1.join();
            writer2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // Concurrent read operation
        Runnable readerTask = () -> {
            for (String item : set) {
                System.out.println(Thread.currentThread().getName() + ": " + item);
            }
        };
        
        Thread reader1 = new Thread(readerTask);
        Thread reader2 = new Thread(readerTask);
        
        reader1.start();
        reader2.start();
    }
}

ConcurrentLinkedQueue

ConcurrentLinkedQueue is an unbounded thread-safe queue based on linked nodes. It is an implementation of a wait-free, lock-free algorithm.

import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentLinkedQueueExample {
    public static void main(String[] args) {
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
        
        // Simulate multiple threads adding to the queue
        Runnable producerTask = () -> {
            for (int i = 0; i < 1000; i++) {
                queue.add(Thread.currentThread().getName() + i);
            }
        };
        
        Thread producer1 = new Thread(producerTask);
        Thread producer2 = new Thread(producerTask);
        
        producer1.start();
        producer2.start();
        
        try {
            producer1.join();
            producer2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // Simulate multiple threads consuming from the queue
        Runnable consumerTask = () -> {
            while (!queue.isEmpty()) {
                String item = queue.poll();
                if (item != null) {
                    System.out.println(Thread.currentThread().getName() + ": " + item);
                }
            }
        };
        
        Thread consumer1 = new Thread(consumerTask);
        Thread consumer2 = new Thread(consumerTask);
        
        consumer1.start();
        consumer2.start();
    }
}

ConcurrentLinkedDeque

ConcurrentLinkedDeque is a thread-safe variant of LinkedList supporting concurrent access to both ends.

import java.util.concurrent.ConcurrentLinkedDeque;

public class ConcurrentLinkedDequeExample {
    public static void main(String[] args) {
        ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<>();
        
        // Simulate multiple threads adding to the deque
        Runnable producerTask = () -> {
            for (int i = 0; i < 1000; i++) {
                deque.addLast(Thread.currentThread().getName() + i);
            }
        };
        
        Thread producer1 = new Thread(producerTask);
        Thread producer2 = new Thread(producerTask);
        
        producer1.start();
        producer2.start();
        
        try {
            producer1.join();
            producer2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // Simulate multiple threads consuming from the deque
        Runnable consumerTask = () -> {
            while (!deque.isEmpty()) {
                String item = deque.pollFirst();
                if (item != null) {
                    System.out.println(Thread.currentThread().getName() + ": " + item);
                }
            }
        };
        
        Thread consumer1 = new Thread(consumerTask);
        Thread consumer2 = new Thread(consumerTask);
        
        consumer1.start();
        consumer2.start();
    }
}

LinkedBlockingQueue

LinkedBlockingQueue is an optionally bounded blocking queue based on linked nodes. It supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.

import java.util.concurrent.LinkedBlockingQueue;

public class LinkedBlockingQueueExample {
    public static void main(String[] args) throws InterruptedException {
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
        
        // Simulate multiple threads producing and consuming
        Runnable producerTask = () -> {
            try {
                for (int i = 0; i < 1000; i++) {
                    queue.put(Thread.currentThread().getName() + i);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };
        
        Runnable consumerTask = () -> {
            try {
                while (true) {
                    String item = queue.take();
                    System.out.println(Thread.currentThread().getName() + ": " + item);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };
        
        Thread producer1 = new Thread(producerTask);
        Thread producer2 = new Thread(producerTask);
        Thread consumer1 = new Thread(consumerTask);
        Thread consumer2 = new Thread(consumerTask);
        
        producer1.start();
        producer2.start();
        consumer1.start();
        consumer2.start();
    }
}

PriorityBlockingQueue

PriorityBlockingQueue is an unbounded blocking queue that uses the same ordering rules as PriorityQueue and supplies blocking retrieval operations.

import java.util.concurrent.PriorityBlockingQueue;

public class PriorityBlockingQueueExample {
    public static void main(String[] args) throws InterruptedException {
        PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();
        
        // Simulate multiple threads producing and consuming
        Runnable producerTask = () -> {
            try {
                for (int i = 0; i < 1000; i++) {
                    queue.put((int) (Math.random() * 1000));
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        Runnable consumerTask = () -> {
            try {
                while (true) {
                    Integer item = queue.take();
                    System.out.println(Thread.currentThread().getName() + ": " + item);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        Thread producer1 = new Thread(producerTask);
        Thread producer2 = new Thread(producerTask);
        Thread consumer1 = new Thread(consumerTask);
        Thread consumer2 = new Thread(consumerTask);

        producer1.start();
        producer2.start();
        consumer1.start();
        consumer2.start();
    }
}

Comparison Table

Here is a comparison table of the different concurrent collections discussed:

Collection Thread-Safe Blocking Operations Use Case
ConcurrentHashMap Yes No High concurrency for key-value pairs
CopyOnWriteArrayList Yes No Frequent reads, infrequent writes
CopyOnWriteArraySet Yes No Thread-safe set with frequent reads
ConcurrentLinkedQueue Yes No High concurrency for FIFO operations
ConcurrentLinkedDeque Yes No High concurrency for double-ended operations
LinkedBlockingQueue Yes Yes Blocking FIFO queue for producer-consumer
PriorityBlockingQueue Yes Yes Blocking queue with priority ordering

Summary

Concurrent collections in Java provide a robust and efficient way to handle multithreaded operations on collections. By using these collections, you can improve the performance and reliability of your applications. Understanding the appropriate use cases and how to implement these collections will help you make better design decisions in your concurrent applications.

Each collection has its own strengths and is designed for specific scenarios. For instance, ConcurrentHashMap is excellent for high-concurrency scenarios with frequent reads and writes, while CopyOnWriteArrayList is better suited for situations with frequent reads but infrequent writes. Similarly, blocking queues like LinkedBlockingQueue and PriorityBlockingQueue are essential for implementing producer-consumer patterns where threads need to wait for available resources.

By choosing the right concurrent collection, you can avoid common concurrency issues such as race conditions, deadlocks, and excessive contention, leading to more scalable and maintainable code.

This completes our exploration of concurrent collections in Java. We hope this article has provided you with a comprehensive understanding of how to use these collections effectively in your multithreaded applications.