Coordination, Waiting & Cancellation
wait() / notify() Without a Condition Loop
wait() / notify() Without a Condition Loop: practice a Java concurrency bug with symptoms like Consumer wakes too early, Strange intermittent behavior,...
- Guarded blocks
- wait/notify
- Guarded Block
- Java
- Beginner
Production symptoms
- Consumer wakes too early
- Strange intermittent behavior
- Logic appears correct at first glance
Failure scenario
Code
synchronized (lock) {
if (job == null) {
lock.wait();
}
String jobToProcess = job;
job = null;
process(jobToProcess);
}
Prod Symptoms
A custom worker handoff uses wait/notify to sleep until work is available. Under concurrent consumers, a thread wakes and proceeds even though another consumer already took the work.
Key signal: wait() can return even when this thread should not proceed. Always re-check the condition.
- Failures happen mostly under bursts or multiple waiting consumers
- A consumer sees null, empty, or stale state after wakeup
- Logs show a notification before the failure, which makes the code look correct
- notifyAll makes the bug easier to trigger when one item wakes many waiters
- Thread dumps may show waiters, but the real bug is the guarded-block protocol
Run Locally
- Both consumers wait for job to become non-null
- The producer publishes one job and calls notifyAll
- One consumer consumes the job
- The other consumer continues without re-checking and then fails with NullPointerException
Inspect hints
- Look for wait() guarded by if instead of while
- Review whether notifyAll can wake more threads than can actually proceed
- Remember that spurious wakeups are allowed, but ordinary competition for the condition is enough to require a loop
javac WaitWithoutLoopDemo.java
java WaitWithoutLoopDemo
import java.util.concurrent.CountDownLatch;
public class WaitWithoutLoopDemo {
private static final Object lock = new Object();
private static final CountDownLatch waiting = new CountDownLatch(2);
private static String job;
public static void main(String[] args) throws Exception {
Thread first = new Thread(WaitWithoutLoopDemo::takeBroken, "consumer-1");
Thread second = new Thread(WaitWithoutLoopDemo::takeBroken, "consumer-2");
first.start();
second.start();
waiting.await();
synchronized (lock) {
job = "job";
System.out.println("producer publishes one job and notifyAll");
lock.notifyAll();
}
first.join();
second.join();
}
private static void takeBroken() {
try {
synchronized (lock) {
if (job == null) {
waiting.countDown();
lock.wait();
}
String value = job;
job = null;
System.out.println(Thread.currentThread().getName()
+ " consumed " + value.toUpperCase());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Note: This demo waits until both consumers are parked, then publishes one job and calls notifyAll.
Diagnosis and fix
Explanation
A notification is not a promise that this thread can proceed. It only means the thread may wake up, reacquire the monitor, and check the shared state again.
Key signal: The correct shape is while (!condition) wait(); then consume the state.
- notifyAll can wake multiple consumers for one available item
- Another consumer can consume the item before this thread resumes
- wait() is also allowed to return without the condition becoming true
- The condition and wait form one guarded-block protocol
- The guard must be while, not if
How to Diagnose
This is mainly a code-review bug. Use runtime evidence to find the path, then inspect the condition protocol.
- Look for wait() guarded by if instead of while
- Check whether the condition tested before wait is exactly the condition needed to proceed
- Check whether notifyAll can wake more threads than there are resources
- Correlate null, empty, or stale-state failures with recent notifications
- If a dump exists, use it to locate monitor waiters, then review the code around wait()
jps
jstack <pid>
jcmd <pid> Thread.print
producer publishes one job and notifyAll
consumer-* consumed JOB
Exception in thread "consumer-*" java.lang.NullPointerException
How to Fix
- Guard wait() with while, not if
- Re-check the real condition after every wakeup
- Mutate the condition and notify while holding the same monitor
- Keep slow processing outside the synchronized block when possible
- Do not replace notifyAll with notify as a substitute for the condition loop
- Prefer BlockingQueue when the problem is producer/consumer handoff
import java.util.concurrent.CountDownLatch;
public class WaitWithLoopFixed {
private static final Object lock = new Object();
private static final CountDownLatch waiting = new CountDownLatch(2);
private static String job;
public static void main(String[] args) throws Exception {
Thread first = new Thread(WaitWithLoopFixed::take, "consumer-1");
Thread second = new Thread(WaitWithLoopFixed::take, "consumer-2");
first.start();
second.start();
waiting.await();
put("job-1");
Thread.sleep(300);
put("job-2");
first.join();
second.join();
}
private static void put(String value) {
synchronized (lock) {
job = value;
lock.notifyAll();
}
}
private static void take() {
try {
String value;
synchronized (lock) {
while (job == null) {
waiting.countDown();
lock.wait();
}
value = job;
job = null;
}
System.out.println(Thread.currentThread().getName()
+ " consumed " + value.toUpperCase());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Note: The second consumer wakes on the first notifyAll, re-checks, and waits again until another item exists.