Is there BlockingMap as BlockingQueue in java?

Learn is there blockingmap as blockingqueue in java? with practical examples, diagrams, and best practices. Covers java, concurrency, java.util.concurrent development techniques with visual explana...

Exploring BlockingMap Alternatives in Java Concurrency

Hero image for Is there BlockingMap as BlockingQueue in java?

While Java's java.util.concurrent package offers robust blocking queues, there's no direct BlockingMap equivalent. This article explores why, and how to achieve similar functionality using existing concurrent constructs.

Java's java.util.concurrent package provides powerful tools for managing concurrency, with BlockingQueue being a cornerstone for producer-consumer patterns. Developers often wonder if a similar BlockingMap exists, offering blocking semantics for map operations like get() or put() when a key is absent or a capacity limit is reached. The short answer is no, there isn't a direct BlockingMap interface or implementation in the standard library. This article delves into the reasons behind this absence and demonstrates how to construct similar functionality using existing concurrent primitives.

Why No Direct BlockingMap?

The primary reason for the absence of a direct BlockingMap lies in the fundamental differences between queues and maps regarding their core operations and use cases. Queues are inherently designed for sequential access (FIFO, LIFO, priority-based) where elements are added and removed, often with a fixed capacity. Blocking behavior in queues typically involves waiting for space to become available (on put) or for an element to be present (on take).

Maps, on the other hand, are designed for random access based on keys. Their primary operations (put, get, remove) are usually expected to complete quickly, either by finding the key or indicating its absence. Introducing blocking semantics to these operations would fundamentally alter their nature and could lead to complex deadlock scenarios or unexpected performance characteristics, especially if multiple threads are waiting on different keys or capacity constraints.

flowchart TD
    A[BlockingQueue] --> B{Sequential Access}
    B --> C[Producer-Consumer]
    C --> D[Blocking on Empty/Full]

    E[Map] --> F{Key-Based Access}
    F --> G[Data Storage/Retrieval]
    G --> H[Non-Blocking by Default]

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style E fill:#bbf,stroke:#333,stroke-width:2px

Conceptual difference between BlockingQueue and Map access patterns.

Implementing BlockingMap-like Behavior

While there's no off-the-shelf BlockingMap, you can achieve similar functionality by combining existing concurrent utilities. The approach depends on the specific blocking behavior you need:

  1. Blocking on Key Absence (Wait for Value): If you want a get(key) operation to block until a value for that key is present.
  2. Blocking on Capacity (Bounded Map): If you want put(key, value) to block when the map reaches a certain size.

Let's explore how to implement these scenarios.

Scenario 1: Blocking on Key Absence (Wait for Value)

This scenario is common when a producer thread computes a value for a key, and a consumer thread needs to wait for that value to become available. A ConcurrentHashMap combined with CompletableFuture or CountDownLatch can effectively achieve this.

Using CompletableFuture is often the most elegant solution, as it represents a future result that can be completed by one thread and waited upon by another. Each key in our 'blocking map' would map to a CompletableFuture.

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class BlockingMapOnKeyAbsence<K, V> {
    private final ConcurrentHashMap<K, CompletableFuture<V>> map = new ConcurrentHashMap<>();

    public void put(K key, V value) {
        CompletableFuture<V> future = map.computeIfAbsent(key, k -> new CompletableFuture<>());
        future.complete(value);
    }

    public V get(K key) throws InterruptedException, ExecutionException {
        CompletableFuture<V> future = map.computeIfAbsent(key, k -> new CompletableFuture<>());
        return future.get(); // Blocks until the future is completed
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        BlockingMapOnKeyAbsence<String, Integer> blockingMap = new BlockingMapOnKeyAbsence<>();

        // Producer thread
        new Thread(() -> {
            try {
                Thread.sleep(2000); // Simulate work
                System.out.println("Producer: Putting value for key 'A'");
                blockingMap.put("A", 100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();

        // Consumer thread
        new Thread(() -> {
            try {
                System.out.println("Consumer: Waiting for value for key 'A'...");
                Integer value = blockingMap.get("A");
                System.out.println("Consumer: Received value for key 'A': " + value);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Implementing blocking get() using CompletableFuture.

Scenario 2: Blocking on Capacity (Bounded Map)

A bounded map that blocks on put() when full is more complex to implement correctly. It requires managing both the map's state and a mechanism for threads to wait and be notified. A common approach involves using a ConcurrentHashMap for storage, combined with a Semaphore or a custom ReentrantLock and Condition to manage capacity.

Using a Semaphore is generally simpler for managing a fixed number of permits (representing available slots). Each successful put acquires a permit, and each remove releases one.

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class BoundedBlockingMap<K, V> {
    private final ConcurrentHashMap<K, V> map;
    private final Semaphore semaphore;
    private final int capacity;

    public BoundedBlockingMap(int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException("Capacity must be positive");
        }
        this.capacity = capacity;
        this.map = new ConcurrentHashMap<>(capacity);
        this.semaphore = new Semaphore(capacity);
    }

    public V put(K key, V value) throws InterruptedException {
        semaphore.acquire(); // Blocks if no permits available (map is full)
        V oldValue = map.put(key, value);
        if (oldValue != null) {
            // If we replaced an existing value, we didn't actually add a new element
            // so release the acquired permit.
            semaphore.release();
        }
        return oldValue;
    }

    public V get(K key) {
        return map.get(key);
    }

    public V remove(K key) {
        V removedValue = map.remove(key);
        if (removedValue != null) {
            semaphore.release(); // Release a permit when an element is removed
        }
        return removedValue;
    }

    public int size() {
        return map.size();
    }

    public static void main(String[] args) throws InterruptedException {
        BoundedBlockingMap<String, Integer> boundedMap = new BoundedBlockingMap<>(2);

        // Fill the map
        System.out.println("Main: Putting A");
        boundedMap.put("A", 1);
        System.out.println("Main: Putting B");
        boundedMap.put("B", 2);

        // This put will block
        new Thread(() -> {
            try {
                System.out.println("Thread-1: Trying to put C (will block)...");
                boundedMap.put("C", 3);
                System.out.println("Thread-1: Successfully put C");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();

        Thread.sleep(100); // Give Thread-1 a chance to block

        // This put will replace B, not block
        new Thread(() -> {
            try {
                System.out.println("Thread-2: Trying to put B (replace)...");
                boundedMap.put("B", 22);
                System.out.println("Thread-2: Successfully put B (replaced)");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();

        Thread.sleep(1000); // Wait a bit

        System.out.println("Main: Removing A");
        boundedMap.remove("A"); // This will unblock Thread-1

        Thread.sleep(1000); // Wait for Thread-1 to complete
        System.out.println("Final map size: " + boundedMap.size());
        System.out.println("Map contents: " + boundedMap.map);
    }
}

Implementing a bounded blocking map using Semaphore.

These examples illustrate that while a direct BlockingMap doesn't exist, Java's rich java.util.concurrent package provides the building blocks to implement custom data structures with blocking semantics tailored to specific needs. The choice of implementation depends heavily on the exact blocking behavior required (e.g., blocking on get for absent keys, or blocking on put for capacity limits).