Thread Communication: wait(), notify(), notifyAll()
Alright, let's move on to Thread Communication: wait()
, notify()
, notifyAll()
. This is how threads can coordinate their activities beyond just mutual exclusion and visibility.
4. Thread Communication: wait()
, notify()
, notifyAll()
Concept:
In many concurrent scenarios, threads don't just need to protect shared resources; they also need to cooperate and communicate with each other. One thread might need to wait for a specific condition to become true, which will be set by another thread.
-
wait()
: Causes the current thread to pause its execution and release the lock it holds on the object. It then waits until another thread callsnotify()
ornotifyAll()
on the same object, and the waiting thread can re-acquire the lock. -
notify()
: Wakes up one arbitrarily chosen thread that is waiting on the object's monitor (lock). -
notifyAll()
: Wakes up all threads that are waiting on the object's monitor. -
Analogy: Our restaurant again, focusing on a Chef and Waiter coordinating an order.
- Shared Resource: The "Order Ready" counter, or the pass-through window where finished dishes are placed.
- Scenario: A waiter (Thread A) takes a customer's order for a specific dish.
- The waiter then goes to the pass-through window and
wait()
s for that dish to be ready. While waiting, the waiter effectively "steps away" from the order-taking counter (releases their lock on it) so other waiters can take new orders. - The chef (Thread B), after cooking the dish, places it on the pass-through window and
notify()
s the waiting waiter that the dish is ready. - The waiting waiter wakes up, "re-acquires" their focus on the order-taking counter (re-acquires the lock), picks up the dish, and serves it.
- The waiter then goes to the pass-through window and
-
Key Rule:
wait()
,notify()
, andnotifyAll()
must always be called from within asynchronized
block (or method) on the object whose intrinsic lock is being used for waiting. If you don't, you'll get anIllegalMonitorStateException
. This is becausewait()
releases the lock, andnotify()
/notifyAll()
signal threads that might be trying to re-acquire it.
"Before" (Problematic Code – Busy Waiting / Polling)
Instead of wait()
/notify()
, a common but inefficient anti-pattern is busy waiting or polling. This is where a thread continuously checks a condition in a loop, consuming CPU cycles unnecessarily.
Scenario: We have a Producer
thread that generates a message, and a Consumer
thread that waits for this message to be available.
Problem: The Consumer
thread will incessantly loop, checking the message
variable, even when no message is available. This wastes CPU cycles (like a waiter constantly peeking into the kitchen, even when nothing is cooking, instead of doing other tasks). This can also lead to visibility issues if message
isn't volatile
, as the Consumer
might never see the updated message.
Java Example (Busy Waiting / Polling):
public class BusyWaitingDemo {
private static String message = null; // Shared variable, initially null
static class Producer implements Runnable {
@Override
public void run() {
try {
System.out.println("Producer: Preparing message...");
Thread.sleep(2000); // Simulate work
message = "Hello from Producer!"; // Produce the message
System.out.println("Producer: Message produced: " + message);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Producer interrupted.");
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
System.out.println("Consumer: Waiting for message (busy-waiting)...");
long startTime = System.nanoTime();
// Busy-wait loop: continuously check if message is not null
while (message == null) {
// This loop is consuming CPU cycles without doing useful work
// It also risks visibility issues if 'message' isn't volatile.
}
long endTime = System.nanoTime();
System.out.println("Consumer: Message received: " + message);
System.out.println("Consumer: Time spent busy-waiting: " + TimeUnit.NANOSECONDS.toMillis(endTime - startTime) + " ms");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Busy-Waiting Demonstration ---");
Thread producerThread = new Thread(new Producer(), "ProducerThread");
Thread consumerThread = new Thread(new Consumer(), "ConsumerThread");
consumerThread.start(); // Start consumer first so it can wait
Thread.sleep(500); // Give consumer time to start its loop
producerThread.start();
producerThread.join(); // Wait for producer to finish
consumerThread.join(); // Wait for consumer to finish
System.out.println("--- Demo Finished ---");
}
}
Likely Output:
You'll observe a delay where the Consumer
thread is "stuck" in its while
loop, continuously checking message
, before it finally prints the message. The "Time spent busy-waiting" will be roughly the same as the producer's sleep time, indicating wasted CPU cycles. If message
were not volatile
(and without any implicit synchronization), the consumer might even wait indefinitely.
--- Busy-Waiting Demonstration ---
Consumer: Waiting for message (busy-waiting)...
Producer: Preparing message...
Producer: Message produced: Hello from Producer!
Consumer: Message received: Hello from Producer!
Consumer: Time spent busy-waiting: 2005 ms // Approx. 2 seconds wasted CPU
--- Demo Finished ---
"After" (Resolved Code – Efficient wait()
/notify()
Communication)
To efficiently handle thread communication and avoid busy-waiting, we use wait()
, notify()
, and notifyAll()
in conjunction with synchronized
blocks.
Resolution: The Producer
will acquire a lock, set the message, and then notify()
the waiting Consumer
. The Consumer
will acquire the same lock, then wait()
on it until it's notified, releasing the lock while waiting. This makes the Consumer
"sleep" and not consume CPU cycles until woken up.
Java Example (Efficient wait()
/notify()
):
import java.util.concurrent.TimeUnit;
public class WaitNotifyDemo {
// The shared resource that Producer produces and Consumer consumes
private static String message = null;
// The lock object for synchronization and wait/notify
private static final Object lock = new Object(); // Crucial: shared lock object
static class Producer implements Runnable {
@Override
public void run() {
try {
System.out.println("Producer: Preparing message...");
Thread.sleep(2000); // Simulate work
// Synchronize on the same lock object as the Consumer
synchronized (lock) {
message = "Hello from Producer!"; // Modify the shared resource
System.out.println("Producer: Message produced: " + message);
lock.notify(); // Notify one waiting Consumer
// Alternatively: lock.notifyAll(); // If multiple consumers were waiting
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Producer interrupted.");
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
System.out.println("Consumer: Waiting for message (wait/notify)...");
long startTime = System.nanoTime();
// Synchronize on the same lock object as the Producer
synchronized (lock) {
// Loop to check the condition. IMPORTANT: Always use wait() in a loop!
// This handles "spurious wakeups" (which we'll cover later)
// and ensures the condition is truly met when woken up.
while (message == null) {
try {
// Releases the 'lock' and waits. This thread becomes non-runnable.
System.out.println("Consumer: Message is null, waiting...");
lock.wait(); // Releases lock and waits until notified or interrupted
System.out.println("Consumer: Woke up. Checking message again...");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Consumer interrupted while waiting.");
return; // Exit if interrupted
}
}
// When loop exits, message is guaranteed to be not null
// The lock is re-acquired by the consumer after it wakes up and before it proceeds.
long endTime = System.nanoTime();
System.out.println("Consumer: Message received: " + message);
System.out.println("Consumer: Total time including wait: " + TimeUnit.NANOSECONDS.toMillis(endTime - startTime) + " ms");
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Wait/Notify Demonstration ---");
Thread producerThread = new Thread(new Producer(), "ProducerThread");
Thread consumerThread = new Thread(new Consumer(), "ConsumerThread");
consumerThread.start(); // Start consumer first
Thread.sleep(500); // Give consumer time to enter wait() state
producerThread.start();
producerThread.join(); // Wait for producer to finish
consumerThread.join(); // Wait for consumer to finish
System.out.println("--- Demo Finished ---");
}
}
Expected Output:
The Consumer
thread will print "Message is null, waiting…" and then effectively pause without consuming CPU. Once the Producer
calls notify()
, the Consumer
will wake up, re-acquire the lock, check the condition, and proceed. The "Total time including wait" will still reflect the waiting period, but the CPU was not consumed during that time.
--- Wait/Notify Demonstration ---
Consumer: Waiting for message (wait/notify)...
Consumer: Message is null, waiting...
Producer: Preparing message...
Producer: Message produced: Hello from Producer!
Consumer: Woke up. Checking message again...
Consumer: Message received: Hello from Producer!
Consumer: Total time including wait: 2005 ms // Approx. 2 seconds, but CPU wasn't busy-waiting
--- Demo Finished ---
Explanation of the Fix and wait()
/notify()
Mechanics:
- Shared Lock Object: Both
Producer
andConsumer
synchronize on the samelock
object. This is fundamental.wait()
,notify()
, andnotifyAll()
are methods ofObject
, and they operate on the intrinsic lock of that specific object. Consumer
wait()
s:- The
Consumer
enters thesynchronized (lock)
block, acquiring the lock. - It checks
while (message == null)
. Since it's initially null, it enters the loop. lock.wait()
is called. This does two critical things:- It atomically releases the
lock
. This is essential so theProducer
can acquire the lock later to modifymessage
and callnotify()
. - It puts the
Consumer
thread into aWAITING
state, making it eligible to be woken up by anotify()
/notifyAll()
onlock
. The thread will not consume CPU cycles while waiting.
- It atomically releases the
- The
Producer
notify()
s:- The
Producer
prepares the message. - It then enters the
synchronized (lock)
block, acquiring the lock (it can do this becauseConsumer
released it). - It sets
message = "..."
. lock.notify()
is called. This moves one (arbitrarily chosen) thread fromlock
's waiting set to its ready queue. TheConsumer
thread becomesRUNNABLE
again, but it doesn't immediately start executing. It has to re-acquire thelock
first.
- The
Consumer
Resumes:- After the
Producer
exits itssynchronized
block (releasinglock
), theConsumer
(nowRUNNABLE
) will contend forlock
. - Once the
Consumer
successfully re-acquires thelock
, it resumes execution from where it left off (afterlock.wait()
). - It then re-evaluates the
while (message == null)
condition. Sincemessage
is now set, the condition is false, and the loop exits. - The
Consumer
proceeds to process the message.
- After the
Production-Grade Practice Points:
- Always call
wait()
,notify()
,notifyAll()
inside asynchronized
block: Failing to do so results inIllegalMonitorStateException
. - Always
wait()
in awhile
loop: This is the golden rule!while (conditionIsNotMet)
while (message == null)
is correct.if (message == null)
is incorrect.- Why? Because of Spurious Wakeups (which we'll cover in detail later, but in short, a thread might wake up without being notified, or another thread might have grabbed the lock and changed the condition before your thread re-acquires it). The loop ensures the condition is re-checked upon waking up.
notify()
vs.notifyAll()
:notify()
: Wakes up one thread. Use it when you know only one thread needs to be woken, or when all waiting threads can perform the same action (e.g., a thread pool waiting for a task). Be careful, as it's arbitrary which thread gets woken.notifyAll()
: Wakes up all threads. Use it when multiple threads might be waiting for different conditions, or when you're unsure which thread needs to be woken. It's safer but can lead to more context switching overhead as many threads wake up and contend for the lock, only to potentially re-wait if their condition isn't met. When in doubt,notifyAll()
is generally safer.
- Don't Use
Thread.sleep()
for Waiting:Thread.sleep()
only pauses the current thread; it does not release any locks. If you usesleep()
inside asynchronized
block, you'll hold the lock unnecessarily, blocking other threads.wait()
releases the lock, which is the key difference and why it's used for inter-thread communication. - Interruption:
wait()
can throw anInterruptedException
. Always handle this exception appropriately, as it's a signal to the thread that it should stop its current activity. - Higher-Level Constructs: For more complex Producer-Consumer scenarios,
java.util.concurrent
package offers much more robust and easier-to-use solutions likeBlockingQueue
andCountDownLatch
, which internally handlewait
/notify
logic. We'll explore these later.
This lesson covered wait()
, notify()
, and notifyAll()
which are fundamental for thread cooperation. Next up, we will discuss Deadlocks, which is a severe consequence of improper locking and resource management.
Ready for Deadlocks?