How do the wait() and notify()/notifyAll() methods work?
- wait(). in short, this method releases the monitor and puts the calling thread into a wait state until another thread calls the notify()/notifyAll() method;
- notify(). Continues the work of a thread whose wait() method was previously called;
- notifyAll() method resumes all threads that have previously had their wait() method called.
public final native void wait(long timeoutMillis) throws InterruptedException; It causes the current thread to wait until it’s awakened. Usually it happens by being notified or interrupted, or until a certain amount of real time has elapsed.
public final void wait() throws InterruptedException. It is no coincidence that we wrote a method without parameters as the second one. In fact, if you look at its code, it refers to the first variant of the method, it just has the 0L argument.
public final wait(long timeout, int nanos). Causes the current thread to wait until it is awakened, typically by being notified or interrupted, or until a certain amount of real time has elapsed.
Wait() method example
Here we’ve got one of the most popular examples that illustrates how the method works. Let's say we have a store, a producer, and a consumer. The manufacturer transfers some products of production to the store, after which the consumer can take them. Let the manufacturer have to produce 8 goods, respectively, the consumer must buy them all. But at the same time, no more than 6 items can be in the warehouse at the same time. To solve this problem, we use the wait() and notify() methods. Let's define three classes: Market, Manufacturer and Client. The Manufacturer in the run() method adds 8 products to the Market object using its put() method. The client in the run() method in a loop calls the get method of the Market object to get these products. The put and get methods of the Market class are synchronized. To track the presence of goods in the Market class, we check the value of the item variable. The get() method for getting a product should only fire if there is at least one product. Therefore, in the get method, we check if the product is missing. If the item is not available, the wait() method is called. This method releases the Market object's monitor and blocks the get method until the notify() method is called on the same monitor. When an item is added in the put() method and notify() is called, the get() method gets the monitor. After that, our client receives an item. To do this, a message is displayed, and the value of the item is decremented. Finally, the notify() method call signals the put() method to continue. In the put() method, similar logic works, only now the put() method should work if there are no more than 6 products in the Market.
class Market {
private int item = 0;
public synchronized void get() {
//here we use wait() method
while (item < 1) {
try {
wait();
}
catch (InterruptedException e) {
}
}
item--;
System.out.println("A client has bought 1 item...");
System.out.println("Items quantity in Market warehouse... " + item);
notify();
}
public synchronized void put() {
//here we use wait() method when the Warehouse is full
while (item >= 6) {
try {
wait();
}
catch (InterruptedException e) {
}
}
item ++;
System.out.println("Manufacturer has added 1 more item...");
System.out.println("Now there are " + item + " items in Warehouse" );
notify();
}
}
class Manufacturer implements Runnable {
Market market;
Manufacturer(Market market) {
this.market = market;
}
public void run() {
for (int i = 0; i < 8; i++) {
market.put();
}
}
}
class Client implements Runnable {
Market market;
Client(Market market) {
this.market = market;
}
public void run() {
for (int i = 0; i < 8; i++) {
market.get();
}
}
}
//wait() method test class
public class WaitTest {
public static void main(String[] args) {
Market market = new Market();
Manufacturer manufacturer = new Manufacturer(market);
Client client = new Client(market);
new Thread(manufacturer).start();
new Thread(client).start();
}
}
Here, using wait() in the get() method, we are waiting for the Manufacturer to add a new item. And after adding, we call notify(), as if to say that one place has become free on the Warehouse, and you can add more.
In the put() method, using wait(), we are waiting for the release of space on the Warehouse. After the space is free, we add the item, notify() starts the thread and the Client can pick up the item.
Here is the output of our program:
In the world of Java multithreading, coordination between threads is essential. One of the key tools that Java provides for this is the wait()
method. But wait—there’s more to it than just making a thread pause. Let’s explore how wait()
works, why it’s used, and how to avoid common pitfalls.
The Sender-Receiver Synchronization Problem
Imagine you’re running a messaging system. A sender thread produces messages, and a receiver thread consumes them. Sounds simple, right? But what if the receiver tries to retrieve a message when there’s nothing to receive? Or what if the sender tries to add a message when the queue is already full?
This is where wait()
comes in. It allows a thread to pause execution until another thread signals that it can proceed. But there’s a catch—spurious wakeups! Sometimes a thread waiting on a condition might wake up unexpectedly without being notified. To avoid errors, we should always enclose wait()
inside a while
loop rather than an if
statement. Let’s see why.
Why Use wait() Inside a While Loop?
Consider this scenario. The receiver thread checks if there’s a message available. If there isn’t, it calls wait()
to pause execution. Later, when the sender adds a message and calls notify()
, the receiver wakes up. But what if another thread removed the message in the meantime? Without rechecking the condition, the receiver might end up trying to retrieve a message that no longer exists.
That’s why the golden rule is: Always enclose wait()
inside a while loop to continuously check the condition before proceeding.
synchronized (lock) {
while (!messageAvailable) {
lock.wait();
}
// Process message
}
By using a while
loop instead of an if
statement, we ensure that the condition is re-checked before resuming execution, preventing race conditions.
Practical Example: Sender-Receiver Communication
Let’s put this theory into action with a real-world example of sender-receiver synchronization using wait()
and notifyAll()
.
class MessageQueue {
private String message;
private boolean hasMessage = false;
public synchronized void send(String msg) {
while (hasMessage) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
message = msg;
hasMessage = true;
notifyAll();
}
public synchronized String receive() {
while (!hasMessage) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
hasMessage = false;
notifyAll();
return message;
}
}
public class SenderReceiverExample {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue();
Thread sender = new Thread(() -> {
String[] messages = {"Hello", "World", "End"};
for (String msg : messages) {
queue.send(msg);
System.out.println("Sent: " + msg);
}
});
Thread receiver = new Thread(() -> {
for (int i = 0; i < 3; i++) {
String msg = queue.receive();
System.out.println("Received: " + msg);
}
});
sender.start();
receiver.start();
}
}
Here’s what happens:
1️⃣ The sender checks if the queue already contains a message. If it does, it waits.
2️⃣ When the sender places a new message, it notifies all waiting threads.
3️⃣ The receiver waits until a message is available. Once it gets notified, it retrieves the message and notifies the sender.
GO TO FULL VERSION