HardExperience level: 3-5 years · 8-15 years4 min read

Producer-Consumer with wait() and notifyAll() — No BlockingQueue

Reviewed by Gurusankar M. · Updated Aug 24, 2026

Asked inAmazonGoogleMicrosoftDeloitte
#producer consumer#wait notify#synchronized#guarded blocks
Report issue

Short Answer

Wrap a fixed-size buffer in a synchronized class. A producer calls wait() while the buffer is full; a consumer calls wait() while it's empty. After adding or removing an item, call notifyAll() to wake any thread that might now be able to proceed. This is exactly the guarded-block mechanism ArrayBlockingQueue implements internally — interviewers ask for it by hand specifically to check you understand wait/notify, not just that you know the java.util.concurrent class name.

On this page

Concept Made Simple

Coffee Chat Question

“How do you implement a producer-consumer queue with wait() and notifyAll() (not BlockingQueue)?”

Remember It Faster

Mind Map Answer

This is the standard guarded-block pattern applied to a bounded buffer: producers block while it's full, consumers block while it's empty, and every state change (a successful add or remove) wakes up threads that might now be able to proceed.

java
class BoundedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;
    BoundedBuffer(int capacity) { this.capacity = capacity; }

    synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait(); // releases the lock, blocks until notified
        }
        queue.add(item);
        notifyAll(); // wake any consumer(s) waiting on "not empty"
    }

    synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        T item = queue.remove();
        notifyAll(); // wake any producer(s) waiting on "not full"
        return item;
    }
}
Producer blocks when
queue.size() == capacity
Consumer blocks when
queue.isEmpty()
After put/take
notifyAll() wakes threads waiting on the OTHER condition
Why while, not if
spurious wakeups + multiple waiters — must re-check after waking

notifyAll() (not notify()) matters here specifically because producers and consumers share the same monitor. notify() wakes one arbitrary waiting thread, which might be another producer that still can't proceed instead of the consumer that just became unblocked. notifyAll() wakes everyone; each re-checks its own while condition, and only the ones that can actually proceed continue — the rest go straight back to waiting.

Why wait() requires the lock, mechanically: wait() is defined to atomically release the monitor and suspend the thread — it has to hold the lock in order to give it up as part of that atomic step, otherwise there'd be a race between "about to wait" and "actually waiting" where a notify() from another thread could slip through and be missed entirely. That's also why notify()/notifyAll() require the lock too, and why calling any of the three outside a synchronized block on that same monitor throws IllegalMonitorStateException — the JVM refuses to run code whose correctness depends on a lock it never proves you're holding.

Spurious wakeups are the JVM/OS being explicitly allowed to wake a waiting thread with no corresponding notify() at all — permitted by the Java spec (and inherited from the underlying OS thread APIs) as an implementation looseness, not a bug to work around defensively "just in case." It's a real, documented possibility, which is exactly why the guard has to be a while loop re-checking the actual condition, rather than an if that trusts a wakeup to mean the condition is now true. An if would let a spuriously-woken producer or consumer barrel ahead on a buffer that's still full or still empty.

Learn by Doing

Hands-on Keyboard

java
BoundedBuffer<Integer> buf = new BoundedBuffer<>(10);
new Thread(() -> { try { buf.put(42); } catch (InterruptedException ignored) {} }).start();
new Thread(() -> { try { System.out.println(buf.take()); } catch (InterruptedException ignored) {} }).start();

Think Beyond the Expected

What If?

You used notify() instead of notifyAll() and the buffer occasionally hangs under load — why?

With multiple producers and consumers waiting on the same monitor, notify() can wake a thread that still can't proceed — say it wakes another blocked producer while a waiting consumer was the one that actually needed the signal. That producer re-checks its while condition, fails it, and goes straight back to wait(), and the consumer that needed waking never gets notified. notifyAll() avoids this by waking every waiter so each can independently re-check its own condition.

Real World

ArrayBlockingQueue and LinkedBlockingQueue implement essentially this pattern internally — production code uses a Condition per direction instead of raw wait/notify for efficiency, but the logic is the same guarded-block shape. Building this by hand once is what lets you actually reason about BlockingQueue's behavior instead of treating it as a black box.

Real Talk from Guru

I still ask this in interviews even though nobody should write it in production — BlockingQueue exists precisely so you don't have to. What I'm checking for is whether someone understands why wait() has to be in a while loop and why notifyAll() beats notify() here, because that understanding is what lets them correctly reason about deadlocks and missed signals anywhere else wait/notify shows up.

Interviewer's Expectation

Keywords they're listening for:

  • wait() releases the lock and blocks; must be called inside synchronized
  • always re-check the condition in a while loop, never an if
  • notifyAll() over notify() when multiple different conditions share one monitor
  • this is the mechanism BlockingQueue implements internally

Common Mistakes

  • Using if instead of while around wait() — misses spurious wakeups and skips the re-check
  • Calling wait()/notify() outside a synchronized block (throws IllegalMonitorStateException)
  • Using notify() when multiple different wait conditions share one monitor

Best Practices

  • Always loop on the condition with while, never if
  • Use notifyAll() unless you can prove only one type of waiter exists on the monitor
  • Reach for java.util.concurrent's BlockingQueue in real production code — build this by hand mainly to internalize the mechanism

Follow-up Questions

  1. Why must wait() be called inside a synchronized block?
  2. What's a spurious wakeup, and why does that alone require a while loop?
  3. How does this relate to ArrayBlockingQueue's actual implementation?
  4. When, if ever, is notify() safe to use instead of notifyAll()?

Continue Learning with AI

Take this question deeper with your favourite AI assistant. Pick a depth, copy the prompt, or open it directly — AI is your learning companion, not a shortcut.

Plain-language foundations

I'm preparing for a software engineering interview and want to understand this from scratch, as a beginner.

Topic: Coordination (Multithreading)
Interview question: "How do you implement a producer-consumer queue with wait() and notifyAll() (not BlockingQueue)?"

Please:
1. Explain the core idea in simple, plain language, using an everyday analogy.
2. Define any technical terms you use.
3. Walk through one small, concrete example.
4. Finish with a single sentence I can easily remember.

Keep the tone friendly and assume I'm new to this topic.
Open inChatGPTGeminiClaude

Was this answer helpful?

Affiliate links

Equipment for a comfortable, productive setup.

As an Amazon Associate I earn from qualifying purchases.