Fundamentals 14 min read

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.

Programmer DD
Programmer DD
Programmer DD
Why Does Concurrent Programming Need Wait/Notify? Unlock Efficient 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencySynchronizationwait/notifyThread CoordinationMESA Model
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.