Skip to Content
Course content

89: Producer-Consumer with BlockingQueue

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started working with multi-threaded Java, I fell into a trap that I see almost every junior developer hit: I thought that a BlockingQueue was just a "thread-safe list" and that I still needed to write my own synchronized blocks and wait()/notify() logic around it to make the Producer and Consumer actually talk to each other.

The Myth: You Need Manual Synchronization Around the Queue

The misconception is that the BlockingQueue only prevents the internal data structure from being corrupted. People think, "Sure, the queue won't crash if two threads touch it, but I still need to tell the Consumer to wake up when the Producer adds something."

If you do this, your code ends up looking like a mess of nested blocks:

// DON'T DO THIS
synchronized(queue) {
    while(queue.isEmpty()) {
        queue.wait();
    }
    T item = queue.take();
}

This is completely redundant. By wrapping a BlockingQueue in a synchronized block, you're essentially putting a lock on a lock. You're adding massive overhead and increasing the risk of deadlocks for absolutely no gain. The "Blocking" part of BlockingQueue is the most important word in the name—it means the coordination is already baked into the methods.

The Reality: BlockingQueue Is a Coordination Tool, Not Just a List

The magic happens in the put() and take() methods. These aren't just getters and setters; they are synchronization points.

  • put(E e): If the queue is full, the producer thread doesn't just throw an error or return false. It literally pauses (blocks) right there until a consumer takes something out and creates space.
  • take(): If the queue is empty, the consumer thread doesn't spin in a while(true) loop wasting CPU cycles. It goes to sleep until the producer puts something in.

I like to think of it as a physical conveyor belt with a limited length. If the belt is full, the person loading it has to stop. If the belt is empty, the person taking things off has to wait. Neither person needs a walkie-talkie to tell the other what to do; the state of the belt tells them everything they need to know.

Let's look at a real-world scenario: a log processing system where one thread reads raw log lines from a file and another thread parses those lines and saves them to a database. We use an ArrayBlockingQueue here to prevent the producer from reading the entire file into memory if the database is slow.

import java.util.concurrent.*;

public class LogProcessor {
    public static void main(String[] args) {
        // Limit the queue to 100 lines to prevent OutOfMemoryError
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);

        // The Producer: Reads the "file"
        Runnable producer = () -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    String log = "Log Entry #" + i;
                    System.out.println("Producing: " + log);
                    queue.put(log); // Blocks if queue is full
                    Thread.sleep(100); // Simulate reading time
                }
                queue.put("POISON_PILL"); // Tell consumer we are done
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        // The Consumer: Writes to "DB"
        Runnable consumer = () -> {
            try {
                while (true) {
                    String log = queue.take(); // Blocks if queue is empty
                    if ("POISON_PILL".equals(log)) break;
                    System.out.println("Consuming: " + log + " -> Saved to DB");
                    Thread.sleep(200); // Simulate DB latency
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

Notice how clean that is. No wait(), no notifyAll(), and no manual locks. The BlockingQueue handles the signaling internally. I used a "Poison Pill" (the POISON_PILL string) because the consumer's take() method will block forever if the producer finishes and the consumer is still waiting. It's a common pattern to send a special signal to gracefully shut down your consumer threads.




📋 Practical Task

Exercise: Implementing a Concurrent Email Dispatcher

You are building a system that sends notification emails. To avoid overloading the SMTP server, you need to decouple the request to send an email from the actual sending process.

Requirements:

  • Create an EmailTask class that holds a recipient email address and a message body.
  • Implement a Dispatcher class that uses a LinkedBlockingQueue (with a capacity of 50) to hold pending EmailTask objects.
  • Create a Producer thread that generates 20 random EmailTask objects and puts them into the queue.
  • Create two Consumer threads (Worker threads) that take() tasks from the queue and print: "Thread [ID] sending email to [email]...".
  • Ensure the program terminates gracefully. You must use the "Poison Pill" strategy: the producer should put two "special" empty EmailTask objects (one for each consumer) into the queue after it finishes producing the 20 emails.

Goal: Verify that both consumer threads process the emails concurrently and both exit the loop when they encounter the poison pill.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.