← Back to list

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…

Cloud Full Stack · 2025-07-14 09:12 · 5 claps · 6.3 min read
#programming #multithreading #java #blockingqueue #synchronization
Open on Medium ↗
Wiki topics: 💻 · Programming 📚 · Books & Reading

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:

  1. ReentrantLock: Provides mutual exclusion (only one thread can modify the queue at a time)
  2. Condition Variables:
  • notFull: Producers wait here when the queue is full
  • notEmpty: 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)

[embed]Java BlockingQueue vs Semaphore Tutorial (2025) Learn how to solve the Producer-Consumer problem in Java using BlockingQueue vs Semaphore with working code.cloudfullstack.dev

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 full
  • notEmpty: 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 available
  • take() blocks until an item is available
  • offer() returns false immediately if queue is full
  • poll() returns null immediately if queue is empty

5. How would you implement a BlockingQueue from scratch?

Key components:

  1. A Queue (like LinkedList) for storage
  2. A ReentrantLock for thread safety
  3. Two Condition variables (notFull, notEmpty)
  4. put() waits on notFull, signals notEmpty
  5. 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.

[embed]Java BlockingQueue vs Semaphore Tutorial (2025) Learn how to solve the Producer-Consumer problem in Java using BlockingQueue vs Semaphore with working code.cloudfullstack.dev


메타데이터
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