Back to wait/notify, sleep/yield & Producer-Consumer
Q3Question 3 of 6

Why must wait() be in a while loop?

Key Concepts
  • wait() must always be in a while loop, never an if statement.
  • Spurious wake-ups: the JVM specification allows wait() to return without notify() being called.
  • Stolen conditions: between notify() and lock re-acquisition, another thread may change the state.
  • The while loop rechecks the condition after every wake-up, catching both spurious and stolen cases.
  • The canonical pattern: synchronized(lock) { while (!condition) { lock.wait(); } doWork(); }
  • Using if instead of while can cause NullPointerException when polling an empty queue.

Definition

wait() must be in a while loop because spurious wake-ups can return without notify(), and the condition may change between notify() and lock re-acquisition.
  • Spurious wake-ups – the JVM specification (JLS 17.8.1) allows wait() to return without notify() being called. This is rare but legal.
  • Stolen conditions – after notify(), the woken thread must re-acquire the lock. During that window, another thread may acquire the lock first and change the state (e.g., consume the last item).
  • while loop – rechecks the condition after every wake-up. If the condition is no longer true, the thread goes back to wait().
  • if statement – checks the condition once. If a spurious wake-up occurs or the condition is stolen, the thread proceeds with a false assumption.

Using if: the manager calls you from the break room saying "A customer arrived!" You walk to the counter (re-acquire lock). But while you were walking, another teller served that customer. You arrive and find no customer. With if, you start processing nothing and crash. With while, you check again: "Is there a customer?" No, so you go back to the break room.

Understanding the Concepts

What goes wrong with if?

Using if checks the condition once. After waking from wait(), the thread proceeds without re-checking. If a spurious wake-up occurred or another thread consumed the item, the condition is no longer true.

JAVA
package com.bank.core;

import java.util.LinkedList;
import java.util.Queue;

public class IfVsWhileDemo {
    private static final Object lock = new Object();
    private static final Queue<String> queue = new LinkedList<>();

    // WRONG: if checks condition once
    public static String wrongConsume() throws InterruptedException {
        synchronized (lock) {
            if (queue.isEmpty()) {
                lock.wait();
            }
            // DANGER: queue might STILL be empty!
            // Spurious wake-up or another consumer took the item.
            return queue.poll(); // returns null -> NullPointerException!
        }
    }

    // CORRECT: while rechecks after every wake-up
    public static String correctConsume() throws InterruptedException {
        synchronized (lock) {
            while (queue.isEmpty()) {
                lock.wait();
            }
            // Guaranteed: queue is NOT empty
            return queue.poll();
        }
    }
}

What is a spurious wake-up?

The JVM specification (JLS 17.8.1) explicitly states: "A thread may also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup." This happens because the underlying OS implementation (pthread_cond_wait on Linux) may return spuriously due to signals or kernel scheduling decisions. It is rare but legally permitted, and code that does not handle it is formally incorrect.

What is a stolen condition?

Even without spurious wake-ups, the condition can change between notify() and lock re-acquisition.

TEXT
Timeline with 3 threads:
1. Consumer-A and Consumer-B both waiting (queue empty)
2. Producer adds one item, calls notifyAll()
3. Consumer-A wakes up, moves to entry set, competes for lock
4. Consumer-B also wakes up, competes for lock
5. Consumer-B acquires lock FIRST, polls the item, releases lock
6. Consumer-A acquires lock, but queue is EMPTY!
   With if:    queue.poll() returns null -> NPE or wrong logic!
   With while: rechecks isEmpty() -> true -> goes back to wait()

What is the canonical pattern?

This pattern works for both wait() and Condition.await(). It is the only correct way to use wait-based synchronization.

JAVA
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    // condition is GUARANTEED true here
    doWork();
}

How does this apply to a banking producer-consumer?

JAVA
package com.bank.queue;

import java.math.BigDecimal;
import java.util.LinkedList;
import java.util.Queue;

public class SettlementQueue {
    private final Object lock = new Object();
    private final Queue<Settlement> queue = new LinkedList<>();
    private static final int MAX_SIZE = 100;

    public void produce(Settlement settlement) throws InterruptedException {
        synchronized (lock) {
            while (queue.size() >= MAX_SIZE) {
                lock.wait();  // wait until space available
            }
            queue.add(settlement);
            lock.notifyAll();  // wake consumers
        }
    }

    public Settlement consume() throws InterruptedException {
        synchronized (lock) {
            while (queue.isEmpty()) {
                lock.wait();  // wait until item available
            }
            Settlement s = queue.poll();
            lock.notifyAll();  // wake producers waiting for space
            return s;
        }
    }
}

How does Condition.await() relate to this pattern?

The Condition interface from java.util.concurrent.locks provides the same wait/notify mechanism with finer-grained control. The same while-loop rule applies.

JAVA
package com.bank.queue;

import java.util.concurrent.locks.*;

public class ConditionQueueExample {
    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    public void produce(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (isFull()) notFull.await();  // while, not if!
            addItem(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
}

What should a senior engineer know?

  • JLS 17.8.1: the specification explicitly allows spurious wake-ups. Any code relying on if instead of while is formally incorrect per the spec.
  • Testing: spurious wake-ups are rare in practice and you cannot reliably trigger them in tests. Code review and static analysis are the primary defenses.
  • FindBugs/SpotBugs: the WA_NOT_IN_LOOP detector flags wait() not inside a while loop. Add this to your CI pipeline.

What are common interview follow-up questions?

  • Does the same rule apply to Condition.await()? Yes, always use while with await().
  • Can you use notifyAll instead of notify? notifyAll is safer because it wakes all waiters. notify wakes only one, which may not be the one whose condition is satisfied.
  • How does BlockingQueue avoid this problem? BlockingQueue handles the while-loop and condition checking internally. Users call put() and take() without writing the loop.

Visual Overview

DIAGRAM
  if (wrong):                   while (correct):
  ┌───────────────────┐         ┌───────────────────┐
  │ check condition    │         │ check condition    │
  │ false: wait()      │         │ false: wait()      │
  │ wakes up           │         │ wakes up           │
  │ proceed (no check!)│         │ recheck condition  │
  │ DANGER: condition  │         │ still false? wait()│
  │ may have changed!  │         │ true? proceed!     │
  └───────────────────┘         └───────────────────┘

Key Takeaway

wait() must always be inside a while loop, never an if statement. Two reasons: spurious wake-ups (the JVM allows wait() to return without notify()) and stolen conditions (another thread changes the state between notify() and lock re-acquisition). The while loop rechecks the condition after every wake-up, catching both cases.

The canonical pattern is: synchronized(lock) { while (!condition) { lock.wait(); } doWork(); }. Using if instead of while can cause NullPointerException, ArrayIndexOutOfBoundsException, or incorrect business logic when the condition is no longer true.

Remember one sentence: always use while (not if) around wait() because spurious wake-ups and stolen conditions can make the condition false when the thread resumes.

---

Share
Use ← → arrow keys to navigate