Topic 98 of 119 11. Multithreading in Java Interactive Tutorial

Inter-Thread Communication in Java: wait(), notify(), and notifyAll()

10 mins read Last updated: 22 Aug 2026 31,954 views

Direct Answer & Core Concept

When multiple threads work together, synchronization alone is sometimes not enough. Threads may also need a way to wait for a condition, signal another thread that something has changed, and coordinate their work.

Java provides three classic methods for this purpose:

  • wait()

  • notify()

  • notifyAll()

These methods are defined by the Object class and work with an object's monitor lock.

They are especially useful for understanding classic coordination patterns such as producer-consumer, where one thread produces data and another thread waits until that data becomes available.

Detailed Explanation & Mechanics

What Is Inter-Thread Communication?

Inter-thread communication is the process through which threads coordinate their execution.

Imagine a producer and consumer:

Main.java
Run Online
Producer Thread | | produces data v Shared Data | | waits until data exists vConsumer Thread

The consumer should not continuously check the data in a tight loop.

Instead, it can wait until the producer signals that the required condition may have changed.

This is one of the purposes of wait() and notification methods.


Why Do Threads Need to Communicate?

Suppose a consumer wants to process an item from a shared buffer.

If the buffer is empty, the consumer cannot continue.

One inefficient approach would be:

Main.java
Run Online
while (buffer.isEmpty()) {
    // keep checking
}

This is called busy waiting or spinning.

The thread repeatedly checks the condition and can waste CPU time.

A better approach is:

Main.java
Run Online
Buffer empty↓Consumer waits↓Producer adds item↓Producer signals↓Consumer wakes↓Consumer checks condition↓Consumer processes item

Java's wait() and notification methods support this style of coordination.


The wait() Method

The wait() method tells the current thread to wait until it is notified or otherwise interrupted/awakened.

A simplified example is:

Main.java
Run Online
synchronized (lock) {
    lock.wait();
}

An important point is that the thread must own the monitor associated with the object on which wait() is called.

For example:

Main.java
Run Online
synchronized (lock) {
    lock.wait();
}

is valid.

But this is not:

when the current thread does not own lock's monitor.

Calling wait() without owning the object's monitor results in IllegalMonitorStateException.


What Happens When wait() Is Called?

When a thread calls:

while holding lock's monitor, several things happen:

  1. The thread enters the object's wait set.

  2. It releases that object's monitor.

  3. It stops executing until it is awakened.

  4. After being awakened, it must reacquire the same monitor before wait() returns.

The release of the monitor is particularly important.

Main.java
Run Online
Thread A | | owns lock | wait() | +---- releases lock | v waiting

Another thread can now acquire the lock and perform the required work.


wait() Releases the Monitor

This is one of the most important differences between wait() and sleep().

When a thread calls:

it releases the monitor associated with lock.

By contrast:

does not release monitors that the thread currently holds.

Comparison

OperationReleases monitor?
wait()Yes
sleep()No
join()The waiting thread may block, but it does not generally release unrelated monitors it holds

For wait(), the monitor release is essential because it allows another thread to acquire the lock and change the condition being waited for.


The notify() Method

The notify() method wakes one thread waiting on the same object's monitor.

Example:

Main.java
Run Online
synchronized (lock) {
    lock.notify();
}

The calling thread must own the monitor.

Main.java
Run Online
Waiting threads:Thread AThread BThread C | notify() | vOne waiting thread is selected

The exact waiting thread selected is not something your program should depend on.

Also, notify() does not immediately transfer the lock to the awakened thread.

The notifying thread still owns the monitor until it exits the synchronized region or otherwise releases the monitor.


The notifyAll() Method

notifyAll() wakes all threads waiting on the same object's monitor.

Main.java
Run Online
synchronized (lock) {
    lock.notifyAll();
}

Conceptually:

Main.java
Run Online
Waiting threads:Thread AThread BThread CThread D | notifyAll() | vAll become eligible to continue

However, they do not all execute inside the synchronized block simultaneously.

Only one thread can own the monitor at a time.

After waking, each thread must reacquire the lock before continuing.


notify() vs notifyAll()

The basic difference is:

Main.java
Run Online
notify()↓Wakes one waiting threadnotifyAll()↓Wakes all waiting threads

A common mistake is assuming that notify() means "wake the thread I want."

It does not provide that kind of selection mechanism.

For many condition-based designs, notifyAll() is safer because all waiting threads get an opportunity to re-check whether their condition is now satisfied.


Why wait() Should Usually Be Inside a while Loop

Consider:

Main.java
Run Online
synchronized (lock) {
    if (!condition) {
        lock.wait();
    } // continue
}

This pattern is generally unsafe.

The preferred structure is:

Main.java
Run Online
synchronized (lock) {
    while (!condition) {
        lock.wait();
    } // continue
}

Why?

Because waking from wait() does not mean the condition is definitely true.

A thread can wake because:

  • Another thread called notify().

  • Another thread called notifyAll().

  • The thread was interrupted.

  • A spurious wakeup occurred.

The condition must therefore be checked again after waking.

The while loop ensures the thread only proceeds when the required condition is actually satisfied.


Basic Example

Here is a simple communication example:

Main.java
Run Online
class Message {
    private String message;
    private boolean available = false;
    public synchronized void produce(String message) throws InterruptedException {
        while (available) {
            wait();
        } this.message = message;
        available = true;
        notifyAll();
    } public synchronized String consume() throws InterruptedException {
        while (!available) {
            wait();
        } String result = message;
        available = false;
        notifyAll();
        return result;
    }
}

The producer waits while the previous message is still available.

The consumer waits while no message is available.

After changing the state, each method calls notifyAll() to allow waiting threads to re-check their conditions.


Producer-Consumer Pattern

The producer-consumer pattern is one of the most common examples of inter-thread communication.

Main.java
Run Online
Producer | | produce v +-------------+ | Buffer | +-------------+ | | consume v Consumer

The producer adds data to the buffer.

The consumer removes data from the buffer.

If the buffer is empty, the consumer waits.

If the buffer is full, the producer waits.


Producer-Consumer Example

Let's build a simple one-item buffer.

Main.java
Run Online
class SharedBuffer {
    private int value;
    private boolean available = false;
    public synchronized void produce(int value) throws InterruptedException {
        while (available) {
            wait();
        } this.value = value;
        available = true;
        System.out.println( "Produced: " + value);
        notifyAll();
    } public synchronized int consume() throws InterruptedException {
        while (!available) {
            wait();
        } int result = value;
        available = false;
        System.out.println( "Consumed: " + result);
        notifyAll();
        return result;
    }
}

Now create producer and consumer threads:

Main.java
Run Online
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();
    }
}

A possible output is:

Main.java
Run Online
Produced: 1Consumed: 1Produced: 2Consumed: 2Produced: 3Consumed: 3Produced: 4Consumed: 4Produced: 5Consumed: 5

The exact scheduling order depends on thread execution.


Understanding the Producer

The producer contains:

Main.java
Run Online
while (available) {
    wait();
}

If the buffer already contains an item, the producer waits.

Once it is allowed to continue, it stores the new value:

Main.java
Run Online
this.value = value;
available = true;

Then:

signals waiting threads that the state has changed.


Understanding the Consumer

The consumer contains:

Main.java
Run Online
while (!available) {
    wait();
}

If there is no item, the consumer waits.

When an item becomes available:

Main.java
Run Online
int result = value;
available = false;

The consumer retrieves it and changes the state.

Then:

allows waiting threads to check their conditions again.


The Monitor Must Match

A crucial rule is that wait(), notify(), and notifyAll() operate on a specific object monitor.

For example:

Main.java
Run Online
synchronized (lock) {
    lock.wait();
}

The same object is involved in both operations.

Main.java
Run Online
synchronized (lock) {
    lock.notifyAll();
}

A thread waiting on lockA will not be awakened by:

because lockA and lockB are different monitor objects.


IllegalMonitorStateException

This code is incorrect:

Main.java
Run Online
Object lock = new Object();
lock.wait();

The current thread does not own the monitor.

You need:

Main.java
Run Online
synchronized (lock) {
    lock.wait();
}

Likewise:

Main.java
Run Online
synchronized (lock) {
    lock.notify();
}

or:

Main.java
Run Online
synchronized (lock) {
    lock.notifyAll();
}

The calling thread must own the object's monitor when invoking these methods.


wait() With a Timeout

Java also provides a timed version:

This means the thread waits for up to the specified time, subject to notification, interruption, and other scheduling conditions.

Example:

Main.java
Run Online
synchronized (lock) {
    while (!ready) {
        lock.wait(1000);
    }
}

The timeout does not remove the need to re-check the condition.


notify() Does Not Release the Lock Immediately

Consider:

Main.java
Run Online
synchronized (lock) {
    lock.notify();
    System.out.println("Still holding lock");
}

Calling notify() wakes a waiting thread, but the current thread still owns the monitor while it remains inside the synchronized region.

The awakened thread must wait until it can reacquire the monitor.

So this:

Main.java
Run Online
notify()↓Waiting thread becomes eligible↓Current thread still owns lock↓Current thread exits synchronized block↓Waiting thread can acquire lock

notifyAll() Does Not Mean All Threads Run Together

Suppose five threads are waiting:

Main.java
Run Online
Thread AThread BThread CThread DThread E

Calling:

makes all of them eligible to compete for the monitor.

It does not mean all five enter the synchronized block simultaneously.

Only one thread can own the monitor at a time.

Each thread must reacquire the lock and re-check its condition.


Why notifyAll() Is Often Safer

Imagine several types of threads are waiting for different conditions on the same monitor.

Using:

might wake a thread whose condition is still false.

That thread checks its condition and waits again.

Another thread whose condition is now true may remain asleep.

Using:

allows all waiting threads to wake and check their conditions.

For this reason, notifyAll() is often a safer choice when multiple condition types share the same monitor.


Spurious Wakeups

A waiting thread can occasionally wake without the specific notification you expected.

Therefore, avoid:

Prefer:

Main.java
Run Online
while (!ready) {
    wait();
}

The loop makes the code robust because the condition is checked again before the thread proceeds.


wait() vs sleep()

These methods are frequently confused.

Featurewait()sleep()
Declared byObjectThread
Requires owning monitorYesNo
Releases object's monitorYesNo
Used for condition coordinationYesNo
Can be timedYesYes
Can be interruptedYesYes

Example:

Main.java
Run Online
synchronized (lock) {
    lock.wait();
}

versus:

wait() is about coordination around a monitor and condition.

sleep() is about temporarily pausing the current thread.


wait() vs notify() vs notifyAll()

A simple way to remember them:

Main.java
Run Online
wait()↓"I cannot continue yet."notify()↓"One waiting thread may try again."notifyAll()↓"All waiting threads may try again."

The waiting threads must still re-check their conditions.


Handling InterruptedException

wait() declares InterruptedException.

A common pattern when you are not able to handle the interruption locally is:

Main.java
Run Online
try {
    wait();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Restoring the interrupt status is often appropriate when the current method cannot fully handle the interruption.

Similarly:

Main.java
Run Online
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Thread interruption is cooperative; it is not a forced termination mechanism.


Example With a Ready Flag

Here is a simple coordination example:

Main.java
Run Online
class Task {
    private boolean ready = false;
    public synchronized void waitUntilReady() throws InterruptedException {
        while (!ready) {
            wait();
        } System.out.println("Task is ready");
    } public synchronized void makeReady() {
        ready = true;
        notifyAll();
    }
}

Usage:

Main.java
Run Online
public class Main {
    public static void main(String[] args) {
        Task task = new Task();
        Thread worker = new Thread(() -> {
            try {
                task.waitUntilReady();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        worker.start();
        Thread controller = new Thread(() -> {
            task.makeReady();
        });
        controller.start();
    }
}

The worker waits until the ready condition becomes true.


A Better Modern Alternative: BlockingQueue

Although wait() and notifyAll() are important concepts to learn, modern Java applications often use higher-level concurrency utilities instead of manually implementing producer-consumer communication.

For example:

Main.java
Run Online
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

Producer:

Consumer:

Main.java
Run Online
int value = queue.take();

The queue handles much of the waiting and notification logic internally.

This is often easier to reason about than manually managing wait() and notifyAll().


Why Learn wait() and notifyAll()?

Even when you use higher-level concurrency APIs, understanding these methods is valuable because they explain fundamental Java concurrency concepts.

You learn:

  • monitor ownership

  • waiting for conditions

  • mutual exclusion

  • thread notification

  • condition re-checking

  • producer-consumer coordination

  • interrupt handling

  • monitor-based synchronization

These concepts also help when reading older Java code that directly uses intrinsic monitors.


Common Mistakes

Mistake 1: Calling wait() Outside Synchronization

Incorrect:

Correct:

Main.java
Run Online
synchronized (lock) {
    lock.wait();
}

Mistake 2: Using if Instead of while

Avoid:

Prefer:

Main.java
Run Online
while (!ready) {
    wait();
}

The condition must be checked again after waking.


Mistake 3: Confusing sleep() With wait()

sleep() does not release the monitor.

wait() releases the monitor associated with the object on which it is called.


Mistake 4: Assuming notify() Chooses a Specific Thread

notify() does not let you specify which waiting thread should wake.

If your design requires all waiting threads to re-check their conditions, notifyAll() may be more appropriate.


Mistake 5: Forgetting to Change the Condition

Calling:

without changing the relevant shared state often does not solve the coordination problem.

Typically, the notifying thread changes the state first:

Main.java
Run Online
ready = true;
notifyAll();

Mistake 6: Using Different Locks

If one thread waits on:

another thread cannot wake it using:

The monitor must be the same object.


Best Practices

1. Always wait in a loop

Use:

Main.java
Run Online
while (!condition) {
    wait();
}

2. Protect the condition with the same monitor

The condition check, state update, waiting, and notification should be designed around the appropriate shared lock.

3. Change state before notifying

A typical pattern is:

Main.java
Run Online
synchronized (lock) {
    condition = true;
    lock.notifyAll();
}

4. Keep synchronization focused

Avoid holding locks while performing unnecessary slow operations.

5. Handle interruption correctly

Do not silently swallow InterruptedException unless there is a deliberate reason.

6. Prefer higher-level concurrency utilities when suitable

For producer-consumer systems, BlockingQueue is often easier and safer than manually implementing the complete coordination mechanism.

Frequently Asked Questions

What is inter-thread communication?

It is a mechanism that allows threads to coordinate their execution based on shared state or conditions.

What are wait(), notify(), and notifyAll()?

They are methods of Object used with an object's monitor to coordinate threads waiting for conditions.

Does wait() release the lock?

Yes. When a thread calls wait() on an object's monitor, it releases that monitor while waiting.

Does sleep() release the lock?

No. Thread.sleep() does not release monitors held by the sleeping thread.

Can wait() be called without synchronized?

Not when using the object's monitor. The current thread must own that object's monitor or IllegalMonitorStateException is thrown.

What does notify() do?

It wakes one thread waiting on the same object's monitor, making that thread eligible to continue after it reacquires the monitor.

What does notifyAll() do?

It wakes all threads waiting on the same object's monitor, making them eligible to compete for the monitor and re-check their conditions.

Why should wait() be inside a while loop?

Because waking does not guarantee that the desired condition is true. The condition must be checked again.

Can notifyAll() run all waiting threads simultaneously?

No. Only one thread can own a particular intrinsic monitor at a time.

Is wait() a method of Thread?

No. wait() is defined by Object, which is why every Java object can participate in this monitor-based coordination mechanism.

Is wait() still commonly used in modern Java?

It remains important for understanding Java concurrency and existing code, but higher-level tools such as BlockingQueue, executors, and other concurrency utilities are often preferable for new application designs.

Suggested Graphic

Create a clean producer-consumer communication diagram:

Main.java
Run Online
PRODUCER THREAD | | produce item v +------------------+ | Shared Buffer | +------------------+ | | item available v CONSUMER THREADIf buffer is empty: Consumer | wait() | v releases lock | | Producer adds item | notifyAll() | v Consumer wakes | Re-check condition | consume()

Highlight the key concept visually:

wait() → release monitor → wait for condition → notifyAll() → reacquire monitor → re-check condition.

Runnable Code Example: Inter-Thread Communication in Java

Java 17+
Main.java
Run Online
class MessageQueue {
    private String message;
    private boolean empty = true;
    public synchronized String take() throws InterruptedException {
        while (empty) {
            wait();
            // Wait inside while loop to guard against spurious wakeups
        } empty = true;
        notifyAll();
        return message;
    } public synchronized void put(String msg) throws InterruptedException {
        while (!empty) {
            wait();
        } this.message = msg;
        this.empty = false;
        notifyAll();
    }
}public class InterThreadDemo {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue();
        new Thread(() -> {
            try {
                queue.put("Order #101 Created");
                System.out.println("Produced message");
            } catch (InterruptedException e) {
            }
        }).start();
        new Thread(() -> {
            try {
                String msg = queue.take();
                System.out.println("Consumed: " + msg);
            } catch (InterruptedException e) {
            }
        }).start();
    }
}

Common Java Technical Interview Questions & Answers

Interview Prep
Q1

Why must wait() always be called inside a while loop instead of an if statement?

To guard against spurious wakeups (where a thread wakes up without an explicit notify) and race conditions where another thread consumed the condition first.
Q2

Why are wait() and notify() methods in java.lang.Object instead of java.lang.Thread?

Because locks and monitors are associated with individual objects, not threads.

Key Takeaways, Best Practices & Clean Code Rules

Java Best Practice & Production Rules

Prefer modern high-level concurrency primitives from java.util.concurrent (like BlockingQueue, Lock, and Condition) over low-level wait/notify.