Java wait(), notify() and notifyAll(): Producer Consumer Guide

Java wait notify explained with wait(), notify() and notifyAll(), a bounded producer consumer queue, spurious wakeups and BlockingQueue.

Monitor of an object with the owner thread, the entry set of blocked threads and the wait set of waiting threads, connected by wait, notify and lock acquire arrows

In Java, the wait(), notify() and notifyAll() methods of Object let threads coordinate on shared state. A thread calls wait() to release an object’s monitor and pause until another thread calls notify() or notifyAll() on the same object.

We use the three methods when one thread must wait for a change made by another thread, for example a consumer that waits for tasks in a queue, or a producer that waits for free space. They are the low-level building blocks behind classes such as BlockingQueue, and we still meet them in older code and in interviews.

The following example lets one thread add a task to taskQueue and notify, while the main thread waits in a loop until the queue is not empty.

List<Integer> taskQueue = new ArrayList<>();
Thread.ofPlatform().start(() -> {
    synchronized (taskQueue) {
        taskQueue.add(7);
        taskQueue.notifyAll();          // wakes the threads waiting on taskQueue
    }
});
int task;
synchronized (taskQueue) {
    while (taskQueue.isEmpty()) {
        taskQueue.wait();               // releases the monitor until notified
    }
    task = taskQueue.remove(0);
}
int received = task;                    // 7

Both threads synchronize on the same object, and the waiting thread checks the condition in a while loop. We first look at what each method does, build a bounded producer and consumer queue, compare notify() with notifyAll() and replace the hand-written code with java.util.concurrent classes. For a shorter comparison with the sleep method, see our sleep vs wait article.

1. What Are wait(), notify() and notifyAll() Methods?

Every Java object has a monitor, and a thread owns it while it runs inside synchronized on that object. The monitor also has a wait set, which is the group of threads that called wait() on the object and have not woken up yet. The three methods are final methods of Object that move threads into and out of that wait set.

Monitor of an object with the owner thread, the entry set of blocked threads and the wait set of waiting threads, connected by wait, notify and lock acquire arrows
A thread that calls wait() moves from the owner slot to the wait set, and notify() moves it back to the queue for the monitor

All three methods have the same rule. The calling thread must own the monitor of the object, or the call throws IllegalMonitorStateException.

1.1. wait()

The wait() method releases the monitor of the object and puts the current thread in the WAITING state. The overloads wait(long timeoutMillis) and wait(long timeoutMillis, int nanos) use TIMED_WAITING and give up after the timeout. The thread leaves the wait set for one of four reasons.

  • Another thread calls notify() and the JVM picks this thread.
  • Another thread calls notifyAll().
  • The timeout ends, if we passed one.
  • Another thread interrupts it, so wait() throws InterruptedException, or it wakes up spuriously, that is, without any of the other reasons.

Before wait() returns, the thread takes the monitor back, so the code after it always runs inside the synchronized block again. Because a thread can wake up while the condition is still false, we always call wait() in a while loop that checks the condition.

1.2. notify()

The notify() method wakes up one thread from the wait set of the object, and the Java specification does not say which one. The call does not release the monitor. The woken thread moves to the queue for the monitor and runs only after the notifying thread leaves its synchronized block.

So if the notifier still works for ten seconds inside its synchronized block after notify(), the woken thread waits those ten seconds too. If no thread is waiting, notify() does nothing and the signal is lost.

1.3. notifyAll()

The notifyAll() method wakes up every thread in the wait set of the object. The threads still run one at a time, because each of them must take the monitor back before wait() returns. Thread priority does not decide the order, so we never write code that depends on which thread runs first.

In a print server, for example, five printer threads wait on a job queue. When an admin cancels all jobs, the queue calls notifyAll() once, and every printer thread wakes up, sees the empty queue and goes back to waiting.

2. How to Use wait(), notify() and notifyAll() in Java

The producer and consumer problem is the classic use case. A producer adds tasks to a queue with a fixed capacity, and a consumer removes them. The producer must wait while the queue is full, and the consumer must wait while it is empty.

The following example keeps the logic in one class, TaskQueue, with a capacity of 2. The producer adds tasks 1 to 5 and the consumer takes five tasks, so both threads finish and we can check the result.

2.1. The Shared Queue

Both methods are synchronized, so they lock the TaskQueue object, and wait() and notifyAll() are called on this. Each method waits in a loop for its own condition and notifies after it changes the list.

class TaskQueue {
    private final List<Integer> tasks = new ArrayList<>();
    private final int capacity;

    TaskQueue(int capacity) {
        this.capacity = capacity;
    }

    synchronized void put(int task) throws InterruptedException {
        while (tasks.size() == capacity) {
            wait();                      // queue full, the producer waits
        }
        tasks.add(task);
        notifyAll();                     // wakes a waiting consumer
    }

    synchronized int take() throws InterruptedException {
        while (tasks.isEmpty()) {
            wait();                      // queue empty, the consumer waits
        }
        int task = tasks.remove(0);
        notifyAll();                     // wakes a waiting producer
        return task;
    }
}

We call notifyAll() here because producers and consumers wait on the same object for different conditions. With notify(), a producer could wake another producer instead of a consumer, which we look at in section 3.

2.2. Producer and Consumer Threads

The producer and the consumer are plain lambdas. Each one catches InterruptedException and restores the interrupt flag, so the thread stops cleanly if someone interrupts it while it waits.

TaskQueue queue = new TaskQueue(2);
List<Integer> consumed = Collections.synchronizedList(new ArrayList<>());

Thread producer = Thread.ofPlatform().name("producer").start(() -> {
    try {
        for (int i = 1; i <= 5; i++) {
            queue.put(i);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

Thread consumer = Thread.ofPlatform().name("consumer").start(() -> {
    try {
        for (int i = 1; i <= 5; i++) {
            consumed.add(queue.take());
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

producer.join();
consumer.join();
List<Integer> result = List.copyOf(consumed);   // [1, 2, 3, 4, 5]

2.3. What Happens at Runtime

The consumer gets the tasks in order, and the list never holds more than two tasks. How often each thread waits differs from run to run, because the scheduler decides when each thread runs.

  • If the consumer starts first, it finds an empty list, calls wait() and releases the monitor.
  • The producer adds task 1 and calls notifyAll(), which moves the consumer to the queue for the monitor.
  • When the list holds two tasks, the producer waits in put() until the consumer removes one and notifies.

Neither thread pauses inside its synchronized block. A pause there keeps the monitor for the whole delay and blocks the other thread for no reason, so we leave delays out of synchronized code.

3. Choosing Between notify() and notifyAll()

The choice depends on who waits on the object. The notifyAll() method is the safe default, and notify() is an optimization for one specific case.

Questionnotify()notifyAll()
Threads wokenOne, chosen by the JVMAll threads in the wait set
Safe when waiters wait for different conditionsNo, it can wake the wrong threadYes
Safe when all waiters are the same kind and one change lets only one continueYesYes, with extra wake-ups
CostOne wake-upEvery waiter wakes, checks the condition and most wait again

Say a TaskQueue with a capacity of 1 has two producers and one consumer, and the queue is full, so both producers wait. The consumer takes the task and calls notify(), which wakes producer A, and comes back for the next task, finds the queue empty and waits. Producer A adds a task, and its notify() wakes producer B instead of the consumer. Producer A tries to add its next task and waits, producer B sees a full queue and waits too, and since nobody is awake, all three threads wait forever.

With notifyAll(), the consumer is always among the woken threads, so it makes progress. If we need targeted wake-ups without that risk, a Lock with two Condition objects gives each kind of thread its own wait set.

4. Replacing wait() and notify() in Java 25 Code

The java.util.concurrent package, added in Java 5, already contains tested versions of most wait and notify patterns. For example, the whole TaskQueue class becomes one line with an ArrayBlockingQueue, whose put() and take() block in the same way.

BlockingQueue<Integer> jobs = new ArrayBlockingQueue<>(2);
jobs.put(1);
jobs.put(2);
boolean added = jobs.offer(3, 50, TimeUnit.MILLISECONDS);   // false, queue full
int first = jobs.take();                                     // 1

When we need our own condition logic, ReentrantLock with Condition replaces the monitor methods. Each Condition has its own wait set, so signal() wakes a thread that waits for that condition only.

class BoundedBuffer {
    private final Deque<Integer> items = new ArrayDeque<>();
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    void put(int item) throws InterruptedException {
        lock.lock();
        try {
            while (items.size() == 2) {
                notFull.await();
            }
            items.addLast(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    int take() throws InterruptedException {
        lock.lock();
        try {
            while (items.isEmpty()) {
                notEmpty.await();
            }
            int item = items.removeFirst();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}
BoundedBuffer buffer = new BoundedBuffer();
buffer.put(4);
int item = buffer.take();               // 4

Most hand-written wait and notify code in business apps falls into four patterns, and each one has a ready-made class.

Old code with wait() and notifyAll() also works on virtual threads. Since Java 24 (JEP 491), a virtual thread that waits inside synchronized releases its carrier thread, so there is no need to rewrite such code only because of virtual threads. Other coordination classes, such as Semaphore and Phaser, are grouped in the Java concurrency guide.

5. wait(), notify() and notifyAll() FAQs

Lost signals, spurious wakeups and the order of woken threads are the parts of wait and notify that interviewers probe most.

5.1. What happens when notify() is called and no thread is waiting?

Nothing. The notification is lost, because the monitor does not remember it. A thread that calls wait() later waits for the next notification, which is why the waiting thread checks a shared condition before it calls wait().

5.2. Can there be a race condition while wait() releases or reacquires the lock?

No. The JVM adds the thread to the wait set and releases the monitor as one atomic step, so a notification sent right after the release reaches the thread. On the way back, wait() returns only after the thread owns the monitor again.

5.3. If a thread receives a notification, is the condition guaranteed to be true?

No. Between the notification and the moment the thread gets the monitor back, another thread can take the monitor first and change the state, for example a second consumer that takes the only task. Spurious wakeups also return without any notification. The while loop checks the condition again and waits once more if it is false.

5.4. Which thread gets the notification when several threads wait?

The specification leaves it open. Which thread notify() wakes depends on the JVM and on timing, and thread priority gives no guarantee. When the choice matters, we call notifyAll() and let each thread check its own condition, or use separate Condition objects.

5.5. Does notifyAll() wake up all threads at the same time?

All waiting threads leave the wait set, but they do not run in parallel. Each one must take the monitor back, so they continue one after another, and only after the notifying thread leaves its synchronized block.

5.6. Why wake up all threads if only one of them can continue?

Because the waiting threads may wait for different conditions, such as producers waiting for space and consumers waiting for tasks. With notifyAll(), the thread that can continue is always woken, and the others go back to waiting after checking their condition.

5.7. Why are wait() and notify() defined in Object and not in Thread?

Because they work with the monitor of an object, and every object has one. A thread waits on the object that holds the shared state, such as a queue, so the methods belong to that object. Calling them on a Thread object is a bad idea, because the JDK uses the Thread monitor for join().

6. Conclusion

The wait() method releases an object’s monitor and pauses the thread, notify() wakes one waiting thread and notifyAll() wakes all of them. Every call needs the monitor of the same object, and every wait() sits in a while loop that checks the condition.

We prefer notifyAll() when threads wait for different conditions, and keep pauses out of synchronized blocks. For new code, BlockingQueue, Lock with Condition, CountDownLatch and CompletableFuture give the same coordination with less room for lost signals.

7. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Hi Lokesh, How to terminate all those threads, after, let’s say, all 30 items where “produced” and “consumed”? Thank you.

  2. Threads are objects so they have lock like other objects. When we use wait() and notify() in synchronized methods, which lock do we release? Thread’s or the object sources which thread use?

    • @mimi, calling wait and notify release object’s lock. Thread don’t have lock associated with it, object’s have lock with it that’s why we call wait(), notify() method on object reference and not thread reference.

  3. This is an excellent article. However, with the above code I get a very orderly production, until the queue is full, and a very orderly consumption, until the queue is empty, and then this repeats.

    The example would be much better if the Thread.sleep() were placed outside the synchronized block, and randomized for a different timeout each time, say, between 500ms and 1000ms.

    The critical section (synchronized block) is simply too large, and by the time the block ends, it repeats immediately and therefore the same thread is likely to reacquire the lock immediately, thus the long strings of production and consumption, without much interleaving. Move the sleep outside and randomize it, and you will have a much better example.

  4. Thank you for the blog!
    I have a question:

    Why do we need to write `while (taskQueue.size() == MAX_CAPACITY)` ?
    Can we use `if` instead of `while`?

  5. Hi,
    Thanks for such a wonderful post.
    In your example both consumer and producer can’t run parallel right?
    To produce or consume they require lock. So if producer is running, consumer can’t process any entry even if the queue is not empty.

  6. Hello Lokesh,my problem is when i run the program post in your blog ,the output is regular,the consumer do not wake up until the queue is full,can you help me?tks
    Produced: 0
    Produced: 1
    Produced: 2
    Produced: 3
    Produced: 4
    Queue is full Producer is waiting , size: 5
    Consumed: 0
    Consumed: 1
    Consumed: 2
    Consumed: 3
    Consumed: 4
    Queue is empty Consumer is waiting , size: 0
    Produced: 5
    Produced: 6
    Produced: 7
    Produced: 8
    Produced: 9
    Queue is full Producer is waiting , size: 5
    Consumed: 5
    Consumed: 6
    Consumed: 7
    Consumed: 8
    Consumed: 9
    Queue is empty Consumer is waiting , size: 0
    Produced: 10
    Produced: 11
    Produced: 12
    Produced: 13
    Produced: 14
    Queue is full Producer is waiting , size: 5
    Consumed: 10
    Consumed: 11
    Consumed: 12
    Consumed: 13
    Consumed: 14
    Queue is empty Consumer is waiting , size: 0
    Produced: 15
    Produced: 16

    • To have the process interleave at all I needed to add a Thread.currentThread().yield(); just inside the while (true) loops of the Consumer and Producer.

  7. Hi Lokesh,
    Can u tell me that what r the conditions to get the object lock. Means can we lock the object through synchronised method and what is the role of wait() method for getting the objects lock.

  8. See i was really devasted and could not understand how to explain this concept in practical scenarios you showed how easily it can be explained

  9. Hello Lokesh,
    I implemented the code mentioned in the blog and mentioned below is the output I was getting

    Produced: 0
    Produced: 1
    Produced: 2
    Produced: 3
    Produced: 4
    Queue is full Producer is waiting , size: 5
    Consumed: 0
    Consumed: 1
    Consumed: 2
    Consumed: 3
    Consumed: 4
    Queue is empty Consumer is waiting , size: 0
    Produced: 5
    Produced: 6
    Produced: 7
    Produced: 8
    Produced: 9
    Queue is full Producer is waiting , size: 5
    Consumed: 5
    Consumed: 6
    Consumed: 7
    Produced: 10
    Produced: 11
    Produced: 12
    Queue is full Producer is waiting , size: 5
    Consumed: 8
    Consumed: 9
    ... 

    Now, the question :
    Q.1
    I hoped for this output only, not the output mentioned in the mentioned in blog as synchronization is on taskQueue (which is being passed from static void main() method) in both the class. So what happens when tProducer.start() happens. It gets the lock of taskQueue object and starts producing and then releases the lock when wait is called. At this time tConsumer which was was in waiting for the lock of taskQueue object in consume() method to get into synchronized block and goes into consuming the item from list and when size is zero it goes into wait and releases the lock and tProducer acquires it and this keeps on going. Am I correct with this understanding ?
    Q.2 I am not able to get what does this “taskQueue.wait()” means in his code. What would have happened if I happen if I use “wait()” or “notifyAll()” only, when I tried it I got this as output after running for a while.

    Consumed: 4
    Queue is empty Consumer is waiting , size: 0
    Exception in thread "Consumer" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:503)
    at in.shubham.howtodoinjava.Consumer.consume(Consumer.java:38)
    at in.shubham.howtodoinjava.Consu...(Consumer.java:23)
    at java.lang.Thread.run(Thread.java:745)
    • 1) You are right.
      2) wait() and notifyAll() must be called on some object, which producer and consumer both can access. So rather than creating a new monitor object for this purpose, I used this existing object.
      Regarding error, not sure how you have modified the wait() and notify() calls. Please share the modified code.

      • Hello Lokesh,

        Firstly thanks for the clearing my doubt. And secondly, I just wrote like this in Producer and Consumer

        this.wait();
           and
         this.notifyAll(); 
      • Thanks for such a quick response and clearing my doubt.
        and what I did in my code was “this.notifyAll()” and “this.wait()” INSTEAD of “taskQueue.wait()” or “taskQueue.notifyAll()”.

        Now that you have told that “wait() and notifyAll() must be called on some object, which producer and consumer both can access” , I somehow think I am able to figure out why error came but it would be really nice if you can elaborate it or what this IllegalMonitorStateException means ?

        • You can’t wait() on an object unless the current thread owns that object’s monitor – otherwise IllegalMonitorStateException is thrown. To own the monitor of that object, you must synchronize on it.

          e.g. So if you want to use this.wait() which I do not recommend, you must do it inside synchronized(this) {...} block.

  10. Thanks for the post. It cleared up many of the confusions I had about threading in general. I just have one quick question about the notify() method. In Producer class after you add(produce) the product into the LinkedList you call notify(), is this to ‘wake up’ the Consumer Thread or Producer Thread? and if it’s for consumer Thread, why so ?

    • I only ask this question because my understanding of notify() is “it wakes up a thread that is waiting on this object’s moniotr”. Thus, since Producer has been put on “waiting” when the list is full, it has to be notified by notify()method within its body to start working again. Is this wrong?

  11. when notify method is called which thread is revoked execution has two ambiguous answers.

    1) definition of notify() : It wakes up the first thread that called wait() on the same object

    2) answer of question: What happens when more than one thread is waiting for notification? Which threads actually get the notification when the notify() method is called?

    Could you please tell me which one is correct.

  12. Nice article.
    It cleared many of my doubts regarding multithreading.

    synchronized (taskQueue)
          {
             while (taskQueue.size() == MAX_CAPACITY)
             {
                System.out.println(&quot;Queue is full &quot; + Thread.currentThread().getName() + &quot; is waiting , size: &quot; + taskQueue.size());
                taskQueue.wait();
             }
               
    

    Like here in above code you associated wait() diirectly on taskqueue object.

    https://www.java-samples.com/showtutorial.php?tutorialid=306
    On this link,wait() is not written like this.wait(). Is wait() directly got link with q object.

  13. really good..most of other learning forums have explained the concept..but here you have put in very simple way. the approach of taking consumer from one Thread class object and Producer as different Thread class object simplified the things and I am able to understand quickly…keep posting..Thanks a lot…

  14. HI,
    Thanks for such an wonderful post. But recently, a question asked to my friend. So, can you help me with that.

    1) Write a small program to create dead lock with wait() and notify().
    2) Write a small programm to create dead lock with synchronixed block().

    If possible please answer.

  15. “By waking up all the threads, we can design the program so that the threads decide among themselves which thread should execute next”
    Can you please explain me how it can be done ? How can the threads decide among themselves ?

    • An example could be group of threads (one thread processing only a specific type of message) watching a message queue. Upon placing a message in queue, producer can notify all consumer threads. Then each consumer thread may check if message is for it, and if it is then process it otherwise wait again.

      This design may look inefficient as correct thread may get chance to check in last; BUT it will start making sense when you start adding more message types and their handler threads into system without modifying code of other handlers.

      Just for an example. Make sense?

  16. I think in this example instead of List, Queue should be used.

    Else each time only element @ location zero will be consumed only.

    • I appreciate the article very much!
      So I’d like to argue for it. Maybe a queue is better to understand here, but there is no doubt that a list can be used as a queue in a proper way (append to rear, get&remove from front), as what the article did. For your question, a consumer indeed consumes only element @location zero each time, and this is also the meaning of First Out for a queue.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.