Fundamentals 14 min read

Java Condition: Fine-Grained Thread Wakeup vs notifyAll Pitfalls

This article demonstrates the evolution from synchronized/wait/notify to Lock/Condition for producer-consumer patterns, showing how notifyAll causes spurious wakeups with multiple threads and how Condition enables precise signaling via separate condition queues for producers and consumers.

samdeepthink
samdeepthink
samdeepthink
Java Condition: Fine-Grained Thread Wakeup vs notifyAll Pitfalls

In a previous article we compared synchronized and Lock, mentioning Condition without detail. This article fills that gap by explaining why Condition exists when Java already provides wait / notify.

Requirement‑Driven Explanation

We start with a simple scenario: a box of capacity 1 , one producer thread (put) and one consumer thread (take). If the box is empty the consumer waits; if full the producer waits. This works perfectly with synchronized / wait / notify:

public class Box {
    private Object item;
    public synchronized void put(Object newItem) throws InterruptedException {
        while (item != null) {
            this.wait();
        }
        item = newItem;
        System.out.println(Thread.currentThread().getName() + " 放入: " + item);
        this.notify();
    }
    public synchronized Object take() throws InterruptedException {
        while (item == null) {
            this.wait();
        }
        Object takenItem = item;
        item = null;
        System.out.println(Thread.currentThread().getName() + " 拿出: " + takenItem);
        this.notify();
        return takenItem;
    }
}

Scaling Up: Capacity 5, Multiple Producers/Consumers

New requirement: increase capacity to 5 and run 3 producers and 3 consumers concurrently. Using wait / notifyAll still works logically but introduces a problem: notifyAll wakes every waiting thread, so threads that should stay asleep are incorrectly awakened. The code below uses notifyAll and logs when a consumer wakes up but finds the box still empty:

public class BoxNew {
    private static final int CAPACITY = 5;
    private final Object[] items = new Object[CAPACITY];
    private int count = 0;
    private int putIndex = 0;
    private int takeIndex = 0;
    public synchronized void put(Object newItem) throws InterruptedException {
        while (count == CAPACITY) {
            wait();
        }
        items[putIndex] = newItem;
        putIndex = (putIndex + 1) % CAPACITY;
        count++;
        System.out.println(who() + " 放入" + newItem + ",当前 " + count + "/" + CAPACITY);
        notifyAll();
    }
    public synchronized Object take() throws InterruptedException {
        while (count == 0) {
            System.out.println(who() + " 盒子空 → wait() 睡觉");
            wait();
            if (count == 0) {
                System.out.println(who() + " 【不该醒】被notifyAll一起叫醒,但盒子仍是空的,回去继续睡");
            }
        }
        Object takenItem = items[takeIndex];
        items[takeIndex] = null;
        takeIndex = (takeIndex + 1) % CAPACITY;
        count--;
        System.out.println(who() + " 取出" + takenItem + ",当前 " + count + "/" + CAPACITY);
        notifyAll();
        return takenItem;
    }
    private static String who() { return Thread.currentThread().getName(); }
    public static void main(String[] args) throws InterruptedException {
        BoxNew box = new BoxNew();
        Runnable consumerTask = () -> {
            try { box.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        };
        Thread c1 = new Thread(consumerTask, "消费者1");
        Thread c2 = new Thread(consumerTask, "消费者2");
        c1.start(); c2.start();
        Thread.sleep(300);
        Thread.currentThread().setName("生产者");
        box.put("货物1");
        Thread.sleep(500);
        box.put("货物2");
        c1.join(); c2.join();
    }
}

Running on Windows 11 with Java 17 produces:

消费者1 盒子空 → wait() 睡觉
消费者2 盒子空 → wait() 睡觉
生产者 放入货物1,当前 1/5
消费者1 取出货物1,当前 0/5
消费者2 【不该醒】被notifyAll一起叫醒,但盒子仍是空的,回去继续睡
消费者2 盒子空 → wait() 睡觉
生产者 放入货物2,当前 1/5
消费者2 取出货物2,当前 0/5

The log line 【不该醒】 proves that notifyAll woke a consumer that should have remained asleep. The while loop acts as a safeguard, but the spurious wakeup still wastes CPU cycles.

Solution: Lock + Condition

Condition

splits a single wait queue into multiple condition queues. Producers wait on producerQueue (box full), consumers wait on consumerQueue (box empty). signal() wakes exactly one thread from the target queue.

public class BoxNew2 {
    private static final int CAPACITY = 5;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition producerQueue = lock.newCondition();
    private final Condition consumerQueue = lock.newCondition();
    private final Object[] items = new Object[CAPACITY];
    private int count = 0;
    private int putIndex = 0;
    private int takeIndex = 0;
    public void put(Object newItem) throws InterruptedException {
        lock.lock();
        try {
            while (count == CAPACITY) {
                producerQueue.await();
            }
            items[putIndex] = newItem;
            putIndex = (putIndex + 1) % CAPACITY;
            count++;
            System.out.println(who() + " 放入 " + newItem + ",当前 " + count + "/" + CAPACITY);
            consumerQueue.signal();
        } finally {
            lock.unlock();
        }
    }
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.println(who() + " 盒子空 → consumerQueue.await() 睡觉");
                consumerQueue.await();
                if (count == 0) {
                    System.out.println(who() + " 【不该醒】被唤醒但盒子仍是空的,回去继续睡");
                }
            }
            Object takenItem = items[takeIndex];
            items[takeIndex] = null;
            takeIndex = (takeIndex + 1) % CAPACITY;
            count--;
            System.out.println(who() + " 取出 " + takenItem + ",当前 " + count + "/" + CAPACITY);
            producerQueue.signal();
            return takenItem;
        } finally {
            lock.unlock();
        }
    }
    private static String who() { return Thread.currentThread().getName(); }
    public static void main(String[] args) throws InterruptedException {
        BoxNew2 box = new BoxNew2();
        Runnable consumerTask = () -> {
            try { box.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        };
        Thread c1 = new Thread(consumerTask, "消费者1");
        Thread c2 = new Thread(consumerTask, "消费者2");
        c1.start(); c2.start();
        Thread.sleep(300);
        Thread.currentThread().setName("生产者");
        box.put("货物1");
        Thread.sleep(500);
        box.put("货物2");
        c1.join(); c2.join();
    }
}

Execution output shows no spurious wakeup line:

消费者1 盒子空 → consumerQueue.await() 睡觉
消费者2 盒子空 → consumerQueue.await() 睡觉
生产者 放入 货物1,当前 1/5
消费者1 取出 货物1,当前 0/5
生产者 放入 货物2,当前 1/5
消费者2 取出 货物2,当前 0/5

Evolution Summary

The three classes form a clear progression:

Box – synchronized + wait / notify; single producer/consumer; correct.

BoxNew – same mechanism but notifyAll; multiple producers/consumers; inevitable spurious wakeups.

BoxNew2 – Lock + dual Condition queues; multiple producers/consumers; zero spurious wakeups (precise signaling).

Three Core Takeaways

while is mandatory, not optional. Both wait() and await() can suffer spurious wakeups; the condition must be re‑checked after every wakeup. Using if is absolutely wrong.

notify() may wake the wrong thread; notifyAll() wakes everyone. With multiple waiters, notify() can wake a thread of the same type (signal loss), while notifyAll() is safe but forces unnecessary wakeups.

Condition’s value is fine‑grained control. By splitting one wait queue into multiple condition queues — producers on producerQueue, consumers on consumerQueue — signal() wakes exactly one thread from the intended queue.

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.

JavaconcurrencyLockProducer-ConsumerReentrantLockConditionSpurious WakeupThread Signaling
samdeepthink
Written by

samdeepthink

Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.

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.