Why Does Concurrent Programming Need Wait/Notify? Unlock Efficient Thread Coordination
This article explains why busy‑waiting loops waste CPU in Java concurrency, introduces the wait/notify mechanism, shows how synchronized, wait() and notify()/notifyAll() work together, highlights common pitfalls such as using if instead of while, and provides practical code examples and best‑practice guidelines for safe thread coordination.
Why Wait/Notify Exists in Concurrent Programming
In a previous article we discussed solving Java deadlocks by breaking the request‑and‑hold condition, which requires a unique ledger manager to obtain all required account books at once. Without a wait/notify mechanism, every worker repeatedly polls the manager in a tight loop, consuming CPU unnecessarily.
while(!accountBookManager.getAllRequiredAccountBook(this, target)) {
// busy‑wait
}If the manager is fast and contention is low, this brute‑force approach may succeed after a few attempts. However, as the manager becomes slower and contention grows, the number of loops can explode to thousands or more, leading to severe CPU waste.
To avoid this, the wait/notify mechanism lets a thread block itself when it cannot obtain the needed resources ( wait()) and lets another thread wake it up when the resources become available ( notify() / notifyAll()).
Wait/Notify Mechanism
Thread A cannot get all account books and blocks itself ( wait).
Thread B releases the needed books and notifies Thread A ( notify / notifyAll).
This pattern eliminates busy‑waiting and saves CPU cycles. Real‑world analogies, such as patients waiting for a doctor’s examination, illustrate the same principle.
Java Keywords for the Mechanism
The built‑in keywords synchronized and the methods wait(), notify(), notifyAll() implement the wait/notify mechanism. The following diagram visualizes the waiting queue:
Key Points
Each lock has its own entry wait queue; different locks do not compete for the same queue. wait() and notify() / notifyAll() must be called inside a synchronized block, and if the lock object is this, you must use this.wait() and this.notify() to avoid java.lang.IllegalMonitorStateException.
Improved AccountBookManager Example
public class AccountBookManager {
List<Object> accounts = new ArrayList<>(2);
synchronized boolean getAllRequiredAccountBook(Object from, Object to) {
while (accounts.contains(from) || accounts.contains(to)) {
try { this.wait(); } catch (Exception e) {}
}
accounts.add(from);
accounts.add(to);
return true;
}
// Return resources
synchronized void releaseObtainedAccountBook(Object from, Object to) {
accounts.remove(from);
accounts.remove(to);
notify();
}
}Two common pitfalls exist in this code:
Pitfall 1 – Using if Instead of while
If the condition is checked with if, a spurious wake‑up or a time gap between notification and reacquiring the lock may cause the condition to be false when the thread resumes, leading to incorrect behavior. The fix is to use a while loop for the condition check.
synchronized boolean getAllRequiredAccountBook(Object from, Object to) {
while (accounts.contains(from) || accounts.contains(to)) {
try { this.wait(); } catch (Exception e) {}
}
accounts.add(from);
accounts.add(to);
return true;
}A thread can transition from waiting to runnable even without a notify() / notifyAll() call (spurious wake‑up). Therefore, always re‑test the condition after waking up.
Pitfall 2 – Using notify() When Multiple Threads May Wait
Calling notify() wakes only a single waiting thread, chosen arbitrarily. If more than one thread is waiting for the same condition, the others may remain blocked indefinitely. Using notifyAll() wakes all waiting threads, each of which re‑checks the condition.
Example with Three Threads
public class NotifyTest {
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
synchronized (resourceA) {
try { resourceA.wait(); } catch (InterruptedException e) {}
}
});
Thread threadB = new Thread(() -> {
synchronized (resourceA) {
try { resourceA.wait(); } catch (InterruptedException e) {}
}
});
Thread threadC = new Thread(() -> {
synchronized (resourceA) {
resourceA.notify(); // change to notifyAll() to wake both A and B
}
});
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
threadA.join();
threadB.join();
threadC.join();
}
}With notify() only one of the waiting threads is awakened, leaving the other stuck. Replacing it with notifyAll() wakes both, allowing the program to finish.
When to Use notify()
The typical use case for notify() is a thread pool where only one waiting thread needs to be awakened because the condition is satisfied for exactly one consumer.
JUC Example – SimpleBlockingQueue
public class SimpleBlockingQueue<T> {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
void enq(T x) {
lock.lock();
try {
while (queueIsFull) {
notFull.await();
}
// enqueue element
notEmpty.signal();
} finally { lock.unlock(); }
}
void deq() {
lock.lock();
try {
while (queueIsEmpty) {
notEmpty.await();
}
// dequeue element
notFull.signal();
} finally { lock.unlock(); }
}
}This model follows the same principle: threads wait on a condition variable and are signaled when the condition becomes true.
MESA Model
The MESA monitor model states that each condition variable has its own waiting queue. In Java’s built‑in monitor, there is a single implicit condition variable: this for synchronized instance methods, the class object for static synchronized methods, or the object used in a synchronized block.
Summary
If contention is low, a simple busy‑wait loop may work, but as contention grows the wait/notify mechanism becomes essential for efficient concurrency. By asking the “four soul questions” you can decide when to replace loops with proper waiting and notification, and you now know the correct usage patterns for wait(), notify(), and notifyAll() in Java.
Further Questions
Why can’t we use notify() for the bank transfer example when multiple threads may need the same resource?
Why does the main thread over now message appear only when notifyAll() is used in the ResourceA example?
References
Java Concurrency in Practice
Java Concurrency in Action
Various online articles on wait/notify differences
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
