Index

Concurrent Collections

Java Collections

13.1 Thread Safety Issues with Collections

In a multithreaded environment, working with shared data structures—like Java collections—can introduce serious problems if not handled carefully. When multiple threads access and modify a collection without synchronization, it can lead to unpredictable behavior, known as thread safety issues.

Understanding Thread Safety

A collection is considered thread-safe if it behaves correctly when accessed from multiple threads, even when at least one of the threads is modifying it. Most standard Java collections, such as ArrayList, HashMap, and HashSet, are not thread-safe. This means that concurrent modifications or iterations over them can result in race conditions, data corruption, and visibility problems.

Race Conditions and Data Corruption

A race condition occurs when multiple threads operate on the same data without proper synchronization, and the final outcome depends on the unpredictable order of execution. For example:

List<Integer> list = new ArrayList<>();

// Thread A
list.add(1);

// Thread B
list.remove(0);

If these operations happen at the same time, the internal structure of the ArrayList can become inconsistent. Since ArrayList is backed by an array, concurrent modifications might lead to incorrect indexing, missed updates, or even ArrayIndexOutOfBoundsException.

Similarly, HashMap is particularly vulnerable during concurrent updates. For instance, if two threads try to resize a HashMap at the same time, it can enter an infinite loop, resulting in 100% CPU usage—an infamous issue prior to Java 8.

Visibility Issues

Even if modifications don’t immediately crash the program, the absence of proper synchronization can lead to visibility problems. One thread might not see the changes made by another thread, due to CPU-level caching or the Java Memory Model. For instance, after adding an element to a list, a second thread might still see the list as empty if changes haven’t been flushed to main memory.

Why Standard Collections Fail

Most collection classes in java.util prioritize performance in single-threaded contexts. Their internal data structures are not guarded against concurrent access. They do not use synchronization or memory barriers, making them unsuitable for multithreaded access without external protection.

Map<String, Integer> map = new HashMap<>();
// Multiple threads accessing `map` can cause unpredictable behavior

Introducing Synchronization

One way to handle thread safety is by synchronizing access to collections manually using synchronized blocks:

synchronized (map) {
    map.put("key", 1);
}

However, this quickly becomes error-prone and inefficient, especially under heavy contention. Moreover, manually synchronizing iteration and modification is complex.

The Need for Concurrent Collections

To solve these problems, Java introduced concurrent collection classes in the java.util.concurrent package. These collections are designed for safe access in concurrent environments without requiring external synchronization.

Examples include:

These classes internally handle locking, memory visibility, and atomicity, making multithreaded programming safer and more manageable.

Conclusion

In summary, improper use of standard collections in concurrent scenarios can lead to severe issues like data corruption and race conditions. Developers must either manually synchronize access or, preferably, use purpose-built concurrent collections to ensure thread safety and program correctness.

Index

13.2 java.util.concurrent Collections (ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue)

Multithreaded programs often need to share and manipulate collections. However, traditional collections like ArrayList and HashMap are not thread-safe by default. Java’s java.util.concurrent package introduces specialized collections designed to work efficiently and safely in concurrent environments. This section explains three of the most commonly used classes: ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue.

ConcurrentHashMap

ConcurrentHashMap<K, V> is a high-performance, thread-safe implementation of the Map interface. It allows concurrent read and write operations without locking the entire map, making it suitable for high-concurrency scenarios like caching, counters, and real-time lookup services.

Internal Mechanism

Prior to Java 8, ConcurrentHashMap achieved concurrency through segment-based locking. The map was divided into multiple segments, and each segment could be locked independently. This allowed multiple threads to operate on different segments in parallel.

From Java 8 onward, ConcurrentHashMap replaced segmented locking with a lock-free read strategy using volatile reads and CAS (Compare-And-Swap) for updates. Write operations use fine-grained synchronization on bins (buckets) instead of segments, and bins may be converted from linked lists to trees (similar to HashMap) to improve performance in high-collision scenarios.

Usage Example

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.put("banana", 2);

// Safe concurrent access
Integer value = map.get("apple");

Best Use Cases

CopyOnWriteArrayList

CopyOnWriteArrayList<E> is a thread-safe variant of ArrayList, optimized for scenarios with many reads and few writes. When an element is added, removed, or updated, the list creates a new internal array, leaving the existing one untouched for readers. This provides snapshot-style immutability to reading threads, avoiding synchronization overhead during reads.

Trade-offs

Usage Example

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");

// Iteration is safe without locking
for (String item : list) {
    System.out.println(item);
}

Best Use Cases

BlockingQueue

BlockingQueue<E> is an interface representing a thread-safe queue that supports blocking operations. Unlike standard queues, its implementations allow producers to wait when the queue is full and consumers to wait when the queue is empty. This makes it ideal for producer-consumer patterns.

Popular implementations include:

Core Methods

Usage Example

BlockingQueue<String> queue = new LinkedBlockingQueue<>();

// Producer thread
new Thread(() -> {
    try {
        queue.put("task");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// Consumer thread
new Thread(() -> {
    try {
        String item = queue.take();
        System.out.println("Consumed: " + item);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();
Click to view full runnable Code

import java.util.concurrent.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ConcurrentCollectionsDemo {

    public static void main(String[] args) throws InterruptedException {
        // 1. ConcurrentHashMap
        System.out.println("=== ConcurrentHashMap Example ===");
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("apple", 1);
        map.put("banana", 2);

        // Safe concurrent access
        Integer value = map.get("apple");
        System.out.println("Value for 'apple': " + value);

        // Concurrent update simulation
        AtomicInteger counter = new AtomicInteger(3);
        Runnable mapWriter = () -> {
            for (int i = 0; i < 5; i++) {
                map.put("key" + i, counter.getAndIncrement());
            }
        };
        Thread t1 = new Thread(mapWriter);
        Thread t2 = new Thread(mapWriter);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final ConcurrentHashMap: " + map);

        // 2. CopyOnWriteArrayList
        System.out.println("\n=== CopyOnWriteArrayList Example ===");
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("A");
        list.add("B");

        // Safe iteration
        for (String item : list) {
            System.out.println("List item: " + item);
        }

        // Add during iteration (won’t affect current iteration)
        for (String item : list) {
            list.add("C");
        }
        System.out.println("Final CopyOnWriteArrayList: " + list);

        // 3. BlockingQueue
        System.out.println("\n=== BlockingQueue Example ===");
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();

        // Producer
        Thread producer = new Thread(() -> {
            try {
                queue.put("task");
                System.out.println("Produced: task");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // Consumer
        Thread consumer = new Thread(() -> {
            try {
                String item = queue.take();
                System.out.println("Consumed: " + item);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

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

Best Use Cases

Summary

Collection Strength Best For
ConcurrentHashMap High-concurrency map operations Real-time lookups, counters
CopyOnWriteArrayList Fast, safe iteration; snapshot semantics Read-heavy, infrequently updated lists
BlockingQueue Built-in blocking support for threads Producer-consumer coordination

By choosing the right concurrent collection, you can simplify thread safety while maintaining performance and correctness in your Java applications.

Index

13.3 Runnable Examples: Basic concurrent collections usage

In this section, we'll explore thread-safe operations using three core concurrent collections: ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue. These examples demonstrate how these collections handle concurrent access safely in multi-threaded environments.

ConcurrentHashMap: Safe Concurrent Updates

import java.util.concurrent.*;

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

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                map.merge("count", 1, Integer::sum); // Atomic update
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start(); t2.start();
        t1.join(); t2.join();

        System.out.println("Final count: " + map.get("count")); // Should be 2000
    }
}

Explanation: The merge() method atomically updates the map. Two threads increment the same key, and ConcurrentHashMap ensures thread safety without explicit synchronization.

CopyOnWriteArrayList: Safe Iteration During Modification

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("A"); list.add("B"); list.add("C");

        // Thread that modifies the list
        new Thread(() -> list.add("D")).start();

        // Safe iteration - works on a snapshot
        for (String item : list) {
            System.out.println("Reading: " + item);
        }
    }
}

Explanation: Unlike ArrayList, CopyOnWriteArrayList allows iteration while modifying the list concurrently. Readers see a consistent snapshot, avoiding ConcurrentModificationException.

BlockingQueue: Producer-Consumer with LinkedBlockingQueue

import java.util.concurrent.*;

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

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

        // Consumer thread
        new Thread(() -> {
            try {
                while (true) {
                    String task = queue.take(); // Blocks if empty
                    System.out.println("Processed: " + task);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }
}

Explanation: The BlockingQueue ensures smooth producer-consumer interaction. put() blocks if the queue is full; take() blocks if it's empty, making it ideal for task pipelines.

Summary

Collection Thread-Safety Example Notes
ConcurrentHashMap Concurrent counter update Atomic operations using merge()
CopyOnWriteArrayList Iteration with concurrent modification Snapshot iteration avoids exceptions
BlockingQueue Task producer-consumer with put/take Blocking behavior simplifies thread control

These collections help eliminate race conditions and data corruption, offering built-in synchronization suited for common concurrent patterns in Java.

Index