wait()
and notify()
mechanisms are fundamental concurrency tools provided by the Object
class. These methods play a crucial role in coordinating the execution of multiple threads, allowing them to communicate and synchronize with each other effectively. By using wait()
and notify()
, developers can solve complex problems related to thread - safe resource sharing, producer - consumer scenarios, and more. This blog post will take an in - depth look at these mechanisms, including their fundamental concepts, usage methods, common practices, and best practices.wait()
MethodThe wait()
method is a method of the Object
class. When a thread calls the wait()
method on an object, it releases the lock it holds on that object and enters the waiting state. The thread will remain in this state until another thread calls the notify()
or notifyAll()
method on the same object.
There are three overloaded versions of the wait()
method:
wait()
: The thread will wait indefinitely until it is notified.wait(long timeout)
: The thread will wait for the specified number of milliseconds. If it is not notified within this time, it will wake up automatically.wait(long timeout, int nanos)
: Similar to wait(long timeout)
, but with more precise time control.notify()
and notifyAll()
Methodsnotify()
: When a thread calls the notify()
method on an object, it wakes up one of the threads that are waiting on that object. The choice of which thread to wake up is non - deterministic.notifyAll()
: This method wakes up all the threads that are waiting on the object. All the awakened threads will then compete for the lock on the object.In Java, every object has an associated monitor (also known as a lock). When a thread enters a synchronized block or method, it acquires the lock on the object. Only one thread can hold the lock at a time. The wait()
, notify()
, and notifyAll()
methods can only be called from within a synchronized block or method because they operate on the object’s monitor.
Here is a simple example to demonstrate the basic usage of wait()
and notify()
:
class SharedObject {
private boolean flag = false;
public synchronized void waitForFlag() throws InterruptedException {
while (!flag) {
wait();
}
System.out.println("Flag is set, proceeding...");
}
public synchronized void setFlag() {
flag = true;
notify();
}
}
public class WaitNotifyExample {
public static void main(String[] args) {
SharedObject shared = new SharedObject();
Thread waitingThread = new Thread(() -> {
try {
shared.waitForFlag();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread notifyingThread = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
shared.setFlag();
});
waitingThread.start();
notifyingThread.start();
}
}
In this example, the waitingThread
calls the waitForFlag()
method, which checks the flag
variable. If the flag
is false
, the thread calls wait()
and enters the waiting state. The notifyingThread
sleeps for 2 seconds and then calls the setFlag()
method, which sets the flag
to true
and calls notify()
to wake up the waiting thread.
The producer - consumer pattern is a classic use case for the wait()
and notify()
mechanisms. In this pattern, producers generate data and put it into a shared buffer, while consumers take data from the buffer. Here is an example:
import java.util.LinkedList;
import java.util.Queue;
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private static final int MAX_SIZE = 5;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait();
}
queue.add(item);
System.out.println("Produced: " + item);
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int item = queue.poll();
System.out.println("Consumed: " + item);
notifyAll();
return item;
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
buffer.produce(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
buffer.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
In this example, the produce()
method checks if the buffer is full. If it is, the producer thread calls wait()
. After adding an item to the buffer, it calls notifyAll()
to wake up any waiting consumer threads. The consume()
method checks if the buffer is empty. If it is, the consumer thread calls wait()
. After consuming an item, it calls notifyAll()
to wake up any waiting producer threads.
while
Loop with wait()
It is recommended to use a while
loop around the wait()
call instead of an if
statement. This is because of the possibility of spurious wakeups. A spurious wakeup occurs when a thread wakes up from the wait()
method without being notified. By using a while
loop, the thread will re - check the condition after waking up.
notifyAll()
SparinglyWhile notifyAll()
can be useful in some cases, it can also cause unnecessary wake - ups and resource consumption. If possible, use notify()
to wake up only one thread.
InterruptedException
ProperlyThe wait()
method throws InterruptedException
. It is important to handle this exception properly. Usually, the best practice is to restore the interrupted status of the thread by calling Thread.currentThread().interrupt()
.
Java’s wait()
and notify()
mechanisms are powerful tools for thread synchronization and communication. They allow multiple threads to coordinate their actions and share resources safely. However, they should be used with care. Understanding the fundamental concepts, following the correct usage methods, and applying best practices can help developers write more robust and efficient concurrent Java programs.
This blog post has provided a comprehensive overview of Java’s wait()
and notify()
mechanisms. By mastering these concepts and practices, you can better handle complex concurrency scenarios in your Java applications.