Java BlockingQueue vs Semaphore: Complete Producer-Consumer Tutorial
The Producer-Consumer problem is everywhere in real programming. We see it in web servers handling requests, message queues processing…
Java BlockingQueue vs Semaphore: Complete Producer-Consumer Tutorial
The Producer-Consumer problem is everywhere in real programming. We see it in web servers handling requests, message queues processing data, and even in simple file downloaders. While we can use basic **synchronized blocks**, they often aren’t enough for serious applications.
Today, we’ll explore two powerful solutions: **BlockingQueue vs [Semaphore](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Semaphore.html)**. We’ll build working examples, measure their performance, and see when to use each one.
Approach 1: BlockingQueue — The Simple Solution
In Java, the BlockingQueue interface from the java.util.concurrent package provides a high-level, thread-safe abstraction for coordinating data exchange between multiple threads. It is especially useful in producer-consumer scenarios where we need to safely share a bounded buffer between producer and consumer threads.
Here’s what makes it special:
- Automatic blocking: Producers wait when the queue is full
- Consumer blocking: Consumers wait when the queue is empty
- Thread-safe: No race conditions or data corruption
- No busy waiting: Threads sleep instead of spinning
Let’s build an example using ArrayBlockingQueue, a fixed-size thread-safe queue implementation:
public class BlockingQueueExample {
private static final int QUEUE_CAPACITY = 5;
private static volatile boolean running = true;
private static long itemsProduced = 0;
private static long itemsConsumed = 0;
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
long startTime = System.currentTimeMillis();
Thread producer = new Thread(() -> {
int value = 0;
try {
while (running && !Thread.currentThread().isInterrupted()) {
queue.put(value);
itemsProduced++;
System.out.println("[Producer] Created item: " + value + " (Queue size: " + queue.size() + ")");
value++;
// Simulate long running work
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Producer interrupted - stopping gracefully");
Thread.currentThread().interrupt();
} finally {
System.out.println("Producer finished. Total items produced: " + itemsProduced);
}
}, "Producer-Thread");
Thread consumer = new Thread(() -> {
try {
while (running && !Thread.currentThread().isInterrupted()) {
int data = queue.take();
itemsConsumed++;
System.out.println("[Consumer] Processed item: " + data + " (Queue size: " + queue.size() + ")");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Consumer interrupted - stopping gracefully");
Thread.currentThread().interrupt();
} finally {
System.out.println("Consumer finished. Total items consumed: " + itemsConsumed);
}
}, "Consumer-Thread");
producer.start();
consumer.start();
Thread.sleep(10000);
running = false;
producer.interrupt();
consumer.interrupt();
producer.join();
consumer.join();
long endTime = System.currentTimeMillis();
System.out.println("\n=== Performance Summary ===");
System.out.println("Total runtime: " + (endTime - startTime) + "ms");
System.out.println("Items produced: " + itemsProduced);
System.out.println("Items consumed: " + itemsConsumed);
System.out.println("Queue final size: " + queue.size());
}
}
The following behaviour can be observed when running the code:
[Producer] Created item: 0 (Queue size: 1)
[Producer] Created item: 1 (Queue size: 2)
[Consumer] Processed item: 0 (Queue size: 1)
[Producer] Created item: 2 (Queue size: 2)
[Producer] Created item: 3 (Queue size: 3)
[Consumer] Processed item: 1 (Queue size: 2)
[Producer] Created item: 4 (Queue size: 3)
[Producer] Created item: 5 (Queue size: 4)
[Producer] Created item: 6 (Queue size: 5)
[Producer] Created item: 7 (Queue size: 5) // Queue is full - producer waits!
[Consumer] Processed item: 2 (Queue size: 4) // Consumer frees up space
[Producer] Created item: 8 (Queue size: 5) // Producer continues
How BlockingQueue Works
BlockingQueue uses three key components to coordinate threads:
- ReentrantLock: Provides mutual exclusion (only one thread can modify the queue at a time)
- Condition Variables:
notFull: Producers wait here when the queue is fullnotEmpty: Consumers wait here when the queue is empty
Array/LinkedList: The actual storage for items
When the queue is full If the producer tries to add an item when the queue is full: → The producer thread automatically pauses (blocks) → It wakes up only when space becomes available → No lost items, no crashes
When the queue is empty If the consumer tries to take an item when empty: → The consumer thread automatically pauses → It wakes up when new items arrive → No busy waiting that wastes CPU
- No Race Conditions: The lock ensures that only one thread modifies the queue
- Efficient Waiting: Threads sleep instead of spinning (no CPU waste)
- Automatic Coordination: Producers and consumers wake each other up
- Fair Access: Threads are woken up in order (no starvation)
Approach 2: Semaphore — The Flexible Solution
In Java, the Semaphore class from the java.util.concurrent package provides a flexible way to control access to shared resources using “permits“. While BlockingQueue abstracts away the low-level synchronisation, using Semaphores gives us full control over the coordination logic.
We’ll use three components:
- emptySlots: Tracks available space (starts at buffer size)
- filledSlots: Tracks available items (starts at 0)
- bufferLock: Ensures only one thread modifies the buffer at a time
public class SemaphoreExample {
private static final int BUFFER_SIZE = 5;
private static volatile boolean running = true;
private static final Queue<Integer> buffer = new LinkedList<>();
private static final Object bufferLock = new Object();
private static final Semaphore emptySlots = new Semaphore(BUFFER_SIZE);
private static final Semaphore filledSlots = new Semaphore(0);
private static long itemsProduced = 0;
private static long itemsConsumed = 0;
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
Thread producer = new Thread(() -> {
int value = 0;
try {
while (running && !Thread.currentThread().isInterrupted()) {
emptySlots.acquire();
synchronized (bufferLock) {
buffer.add(value);
itemsProduced++;
System.out.println("[Producer] Created item: " + value + " (Buffer size: " + buffer.size() + ", Empty slots: " + emptySlots.availablePermits() + ")");
value++;
}
filledSlots.release();
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("Producer interrupted - stopping gracefully");
Thread.currentThread().interrupt();
} finally {
System.out.println("Producer finished. Total items produced: " + itemsProduced);
}
}, "Producer-Thread");
Thread consumer = new Thread(() -> {
try {
while (running && !Thread.currentThread().isInterrupted()) {
filledSlots.acquire();
int data;
synchronized (bufferLock) {
data = buffer.remove();
itemsConsumed++;
System.out.println("[Consumer] Processing item: " + data + " (Buffer size: " + buffer.size() + ", Filled slots: " + filledSlots.availablePermits() + ")");
}
// Signal that slot is now empty
emptySlots.release();
Thread.sleep(1000);
System.out.println("[Consumer] Finished processing item: " + data);
}
} catch (InterruptedException e) {
System.out.println("Consumer interrupted - stopping gracefully");
Thread.currentThread().interrupt();
} finally {
System.out.println("Consumer finished. Total items consumed: " + itemsConsumed);
}
}, "Consumer-Thread");
producer.start();
consumer.start();
Thread.sleep(10000);
running = false;
producer.interrupt();
consumer.interrupt();
producer.join();
consumer.join();
long endTime = System.currentTimeMillis();
System.out.println("\n=== Performance Summary ===");
System.out.println("Total runtime: " + (endTime - startTime) + "ms");
System.out.println("Items produced: " + itemsProduced);
System.out.println("Items consumed: " + itemsConsumed);
System.out.println("Buffer final size: " + buffer.size());
}
}
When the code is executed, we observe the following:
[Producer] Created item: 0 (Buffer size: 1, Empty slots: 4)
[Producer] Created item: 1 (Buffer size: 2, Empty slots: 3)
[Consumer] Processing item: 0 (Buffer size: 1, Filled slots: 0)
[Producer] Created item: 2 (Buffer size: 2, Empty slots: 3)
[Consumer] Finished processing item: 0
[Producer] Created item: 3 (Buffer size: 3, Empty slots: 2)
[Consumer] Processing item: 1 (Buffer size: 2, Filled slots: 1)
[Producer] Created item: 4 (Buffer size: 3, Empty slots: 2)
[Producer] Created item: 5 (Buffer size: 4, Empty slots: 1)
[Consumer] Finished processing item: 1
[Producer] Created item: 6 (Buffer size: 5, Empty slots: 0)
[Producer] Created item: 7 (Buffer size: 5, Empty slots: 0)
[Consumer] Processing item: 2 (Buffer size: 4, Filled slots: 2)
[Producer] Created item: 8 (Buffer size: 5, Empty slots: 0)
[Consumer] Finished processing item: 2
How Semaphore Coordination Works
The producer wants to add an item:
- Calls
emptySlots.acquire()– waits if the buffer is full - Locks buffer and adds an item
- Calls
filledSlots.release()– signals consumer
Consumer wants to take the item:
- Calls
filledSlots.acquire()– waits if the buffer is empty - Locks buffer and removes the item
- Calls
emptySlots.release()– signals producer
This creates a perfect dance — producers and consumers coordinate automatically!
Key Observations:
- Permit Tracking: We can see exactly how many empty slots remain
- Buffer Management: Buffer size changes as items are added/removed
- Automatic Blocking: When empty slots reach 0, the producer waits
- Performance: Producer creates ~18 items, consumer processes ~9 (due to speed difference)
Common BlockingQueue and Semaphore Interview Questions
Here are some frequently asked interview questions and concepts related to both:
1. How does BlockingQueue handle thread synchronisation internally?
BlockingQueue uses a ReentrantLock for mutual exclusion and two Condition variables:
notFull: Producers wait here when the queue is fullnotEmpty: Consumers wait here when the queue is empty
When a producer adds an item, it signals notEmpty. When a consumer removes an item, it signals notFull.
2. What happens if multiple producers and consumers use the same BlockingQueue?
The queue remains thread-safe due to locks and conditions. However, the execution order depends on JVM thread scheduling.
3. Can BlockingQueue cause a deadlock? If yes, how?
Generally, no, because it uses proper lock ordering and condition waiting. Deadlocks can occur if you hold other locks while calling blocking methods:
// DANGER: Potential deadlock
synchronized(lockA) {
synchronized(lockB) {
queue.put(item); // If this blocks, we're holding two locks!
}
}
4. What’s the difference between put()/take() and offer()/poll()?
put()blocks until space is availabletake()blocks until an item is availableoffer()returns false immediately if queue is fullpoll()returns null immediately if queue is empty
5. How would you implement a BlockingQueue from scratch?
Key components:
- A Queue (like LinkedList) for storage
- A ReentrantLock for thread safety
- Two Condition variables (notFull, notEmpty)
- put() waits on notFull, signals notEmpty
- take() waits on notEmpty, signals notFull
6. Can a Semaphore cause a deadlock? How?
Yes, if threads acquire semaphores in different orders:
// Thread 1: acquire A, then B
semaphoreA.acquire();
semaphoreB.acquire();
// Thread 2: acquire B, then A
semaphoreB.acquire();
semaphoreA.acquire(); // DEADLOCK!
If both threads hold one semaphore and wait for the other, a deadlock occurs. Always acquire semaphores in the same order across all threads.
Conclusion
We’ve covered two powerful approaches to the Producer-Consumer problem:
- BlockingQueue: Simple, efficient, perfect for most use cases
- Semaphore: Flexible, controllable, great for custom patterns
For a more detailed explanation and full source code, you can visit the following link.
메타데이터
- post_id
- c6de375e3ac8
- slug
- java-blockingqueue-vs-semaphore-complete-producer-consumer-tutorial-c6de375e3ac8
- url
- https://medium.com/@cloudfullstack/java-blockingqueue-vs-semaphore-complete-producer-consumer-tutorial-c6de375e3ac8
- canonical_url
- https://medium.com/@cloudfullstack/java-blockingqueue-vs-semaphore-complete-producer-consumer-tutorial-c6de375e3ac8
- author_url
- https://medium.com/@cloudfullstack
- status
- ok
- fetched_at
- 2026-08-02 03:37:55