🚀AdvancedAdvanced Concurrency

Concurrent Collections

Use ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, and other thread-safe collections.

Overview

Java provides a set of thread-safe collection classes in java.util.concurrent specifically designed for concurrent access. They outperform synchronized wrappers (like Collections.synchronizedMap) because they use fine-grained locking or lock-free algorithms instead of locking the entire structure for every operation. The key classes are: - ConcurrentHashMap: high-throughput thread-safe map - CopyOnWriteArrayList and CopyOnWriteArraySet: safe for read-heavy, write-rare lists - ArrayBlockingQueue, LinkedBlockingQueue: bounded/unbounded FIFO queues used in producer-consumer designs - ConcurrentLinkedQueue: unbounded non-blocking queue - ConcurrentSkipListMap and ConcurrentSkipListSet: concurrent sorted structures

Key Points

  • •ConcurrentHashMap allows concurrent reads and partial writes without a global lock
  • •ConcurrentHashMap.computeIfAbsent(key, fn) atomically inserts if missing
  • •ConcurrentHashMap does not allow null keys or null values
  • •CopyOnWriteArrayList creates a new array copy on every write; safe for infrequent writes and frequent iteration
  • •BlockingQueue.put() blocks when the queue is full; take() blocks when empty
  • •LinkedBlockingQueue has an optional capacity; ArrayBlockingQueue always has a fixed capacity
  • •ConcurrentLinkedQueue is non-blocking and unbounded, using CAS internally
  • •Collections.synchronizedMap wraps any map but locks the entire map per operation; avoid for high-concurrency
  • •ConcurrentHashMap.merge(key, value, remappingFn) is atomic for frequency-map patterns

Examples

ConcurrentHashMap for concurrent word frequency counting

import java.util.*;
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
        ConcurrentHashMap<String, Integer> freq = new ConcurrentHashMap<>();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 3; i++)
                freq.merge(words[i], 1, Integer::sum);
        });
        Thread t2 = new Thread(() -> {
            for (int i = 3; i < 6; i++)
                freq.merge(words[i], 1, Integer::sum);
        });

        t1.start(); t2.start();
        t1.join(); t2.join();

        freq.forEach((word, count) ->
            System.out.println(word + ": " + count));
    }
}

merge(key, value, fn) is atomic: if the key is absent, it inserts value; if present, it applies fn to (oldValue, value). This avoids a manual get-then-put race.

ArrayBlockingQueue as a producer-consumer channel

import java.util.concurrent.*;

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

        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    queue.put(i);
                    System.out.println("Produced: " + i);
                }
                queue.put(-1); // poison pill
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    int item = queue.take();
                    if (item == -1) break;
                    System.out.println("Consumed: " + item);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

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

The poison pill pattern (-1) signals the consumer to stop. BlockingQueue handles the synchronization automatically; no explicit locking needed. put() blocks if the queue is full, take() blocks if empty.

CopyOnWriteArrayList for event-listener registries

import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

public class EventBus {
    private final CopyOnWriteArrayList<Consumer<String>> listeners =
        new CopyOnWriteArrayList<>();

    public void subscribe(Consumer<String> listener) {
        listeners.add(listener);
    }

    public void publish(String event) {
        // Iterating CopyOnWriteArrayList is safe even if listeners are added concurrently
        for (Consumer<String> listener : listeners) {
            listener.accept(event);
        }
    }

    public static void main(String[] args) {
        EventBus bus = new EventBus();
        bus.subscribe(e -> System.out.println("Listener A: " + e));
        bus.subscribe(e -> System.out.println("Listener B: " + e));
        bus.publish("click");
        bus.publish("submit");
    }
}

CopyOnWriteArrayList shines when iteration is far more frequent than mutation. Adding a listener is O(n) because the entire array is copied, but iteration never throws ConcurrentModificationException.

Common Mistakes

  • Using Collections.synchronizedMap in performance-critical concurrent code. It locks the entire map on every get/put, serializing all threads. Use ConcurrentHashMap instead.
  • Null keys or values in ConcurrentHashMap. Unlike HashMap, ConcurrentHashMap throws NullPointerException for null keys or values. Use a sentinel value (like an empty string) if you need to represent absent.
  • Iterating a synchronized wrapper without holding the lock. Even with Collections.synchronizedMap, iteration is not thread-safe unless you synchronize the entire iteration block. ConcurrentHashMap iterators are weakly consistent and do not throw.
  • Treating CopyOnWriteArrayList as efficient for write-heavy workloads. Every add or remove copies the entire backing array. Use only when reads vastly outnumber writes.

Note

For thread-safe lazy initialization of a ConcurrentHashMap entry, use computeIfAbsent(key, k -> new ArrayList<>()) rather than a manual get-then-put pattern. computeIfAbsent is atomic and avoids constructing the value object unnecessarily if the key already exists.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What happens when you call put() on a full BlockingQueue?