Is there BlockingMap as BlockingQueue in java?
Categories:
Exploring BlockingMap Alternatives in Java Concurrency

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:
- Blocking on Key Absence (Wait for Value): If you want a
get(key)
operation to block until a value for that key is present. - 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.
java.util.concurrent
primitives correctly is crucial.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
.
BoundedBlockingMap
example with Semaphore
has a nuance: if put
replaces an existing key, it doesn't consume a new permit. The semaphore.release()
call handles this. However, if remove
is called on a non-existent key, it will incorrectly release a permit, potentially over-counting available slots. Careful handling of remove
and put
semantics is crucial for robust implementations.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).