← Back to Multithreading Course

PART 4 · CLASS 21

Understanding the Producer–Consumer Problem

Imagine one thread producing data while another thread consumes it. What happens if the consumer is ready but there is no data available? Let's understand this common multithreading problem with a simple example.

1. What Is the Producer–Consumer Problem?

The Producer–Consumer problem is a common situation in multithreaded applications.

We have two types of threads:

  • Producer: A thread that creates or produces data.
  • Consumer: A thread that takes and processes the data produced by another thread.

Both threads work with a shared resource called a buffer.

Think of a buffer as a temporary storage area where produced data is kept until a consumer is ready to process it.

For example, imagine a restaurant kitchen.

  • The chef prepares food. This is the producer.
  • The serving staff takes prepared food to customers. This is the consumer.
  • The counter where prepared food is kept is the buffer.

The chef and serving staff work independently, but they need to coordinate with each other.

2. What Problems Can Occur?

Let's understand two important situations.

Situation 1: The Buffer Is Empty

Imagine the consumer is ready to take some data, but the producer has not produced anything yet.

What should the consumer do?

It cannot consume data that does not exist. It should wait until the producer adds something to the buffer.

Situation 2: The Buffer Is Full

Now imagine the producer is continuously creating data, but the consumer is processing it slowly.

Eventually, the buffer becomes full.

The producer should not keep adding data because there is no available space.

It should wait until the consumer removes some data from the buffer.

So, we have two important conditions:

  • If the buffer is empty, the consumer must wait.
  • If the buffer is full, the producer must wait.

3. Why Do We Need Thread Coordination?

Remember, our producer and consumer are two independent threads.

We cannot simply assume that the producer will always finish its work before the consumer starts.

Both threads may run at different speeds.

For example:

  • The producer may generate 10 items per second.
  • The consumer may process only 5 items per second.
  • The buffer may gradually become full.

Or the opposite may happen:

  • The producer may generate only 2 items per second.
  • The consumer may be ready to process 10 items per second.
  • The consumer may frequently find the buffer empty.

This is why producer and consumer threads need a way to coordinate their work.

4. Understanding the Flow

Let's look at the basic flow of a producer and a consumer.

Producer's Responsibility

  1. Check whether the buffer has space.
  2. If the buffer is full, wait.
  3. Produce an item.
  4. Place the item in the buffer.
  5. Notify the consumer that data is available.

Consumer's Responsibility

  1. Check whether the buffer contains data.
  2. If the buffer is empty, wait.
  3. Remove an item from the buffer.
  4. Process the item.
  5. Notify the producer that space is available.

Notice that both threads need to check a condition before performing their work.

The producer checks whether space is available, while the consumer checks whether data is available.

5. A Simple Java Example

Let's build a small example to understand the interaction between a producer and a consumer.

To keep things simple, we will use a buffer that can hold only one item.

class SharedBuffer {

    private Integer item;

    public synchronized void produce(int value)
            throws InterruptedException {

        while (item != null) {
            wait();
        }

        item = value;

        System.out.println("Produced: " + value);

        notifyAll();
    }

    public synchronized int consume()
            throws InterruptedException {

        while (item == null) {
            wait();
        }

        int value = item;

        item = null;

        System.out.println("Consumed: " + value);

        notifyAll();

        return value;
    }
}

public class Main {

    public static void main(String[] args) {

        SharedBuffer buffer = new SharedBuffer();

        Thread producer = new Thread(() -> {

            try {
                for (int i = 1; i <= 5; i++) {
                    buffer.produce(i);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

        });

        Thread consumer = new Thread(() -> {

            try {
                for (int i = 1; i <= 5; i++) {
                    buffer.consume();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

        });

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

Let's understand what is happening.

Inside produce()

while (item != null) {
    wait();
}

item = value;

notifyAll();

The producer first checks whether the buffer already contains an item.

If it does, the buffer is full, so the producer waits.

Once the buffer becomes empty, the producer adds the new item and notifies waiting threads.

Inside consume()

while (item == null) {
    wait();
}

int value = item;

item = null;

notifyAll();

The consumer first checks whether the buffer contains an item.

If it is empty, the consumer waits.

Once an item becomes available, the consumer takes it and makes the buffer empty again.

It then notifies waiting threads that the buffer may have space available.

6. Why Are We Using synchronized, wait() and notifyAll()?

Let's connect this example with what we learned in our previous classes.

  • synchronized: Protects the shared buffer so only one thread at a time can execute these methods on the same object.
  • wait(): Allows a thread to wait when the required condition is not satisfied. It also releases the object's monitor lock.
  • notifyAll(): Wakes up threads waiting on the same object's monitor so they can check their conditions again.

We use while instead of if because a thread must check the condition again after waking up.

This is important because waking up does not guarantee that the required condition is still true.

7. What Did We Learn?

We have understood the basic Producer–Consumer problem. It is not just about producing and consuming data.

It is about coordinating two or more threads that depend on the same shared resource.

The producer must wait when there is no space, and the consumer must wait when there is no data.

In our example, we used synchronized methods, wait() and notifyAll() to coordinate their work.

There is another useful Java tool that can make this problem much easier to solve: BlockingQueue.

We will explore it in our next class.

Test Your Understanding

Let's see how well you understood this class!

Question 1: What does a producer thread do?

Question 2: What does a consumer thread do?

Question 3: What should a consumer do when the buffer is empty?

Question 4: What should a producer do when the buffer is full?

Question 5: Why do we use wait() in the Producer–Consumer example?

Class Summary

Let's quickly revise what we learned today:

  • The Producer–Consumer problem involves two types of threads.
  • The producer creates data.
  • The consumer takes and processes data.
  • A buffer is a temporary storage area for produced data.
  • The consumer waits when the buffer is empty.
  • The producer waits when the buffer is full.
  • We can coordinate these threads using synchronized, wait() and notifyAll().
  • Both threads must check their conditions again after waking up.

The important lesson is that threads do not always work independently. Sometimes they need to coordinate their activities to complete a task correctly.

NEXT CLASS · CLASS 22

Producer–Consumer Using BlockingQueue

In today's example, we manually managed the buffer using synchronized methods, wait() and notifyAll().

But Java provides a ready-made solution called BlockingQueue.

In our next class, we will learn how BlockingQueue simplifies producer–consumer communication and handles waiting automatically.

0% Completed

You have completed 21 out of 46 classes.