Using Condition to Replace wait/notify for Safer and Faster Java Thread Communication
This article explains why Java's Condition class should replace wait/notify for thread communication, demonstrates the thread‑livelock problem with notify, shows how Condition avoids it, compares performance with notifyAll, and provides complete code examples for producer‑consumer scenarios.
Condition, introduced in JDK 1.5, offers a more reliable way to perform thread communication compared with the traditional wait and notify methods of Object .
Two reasons are given for preferring Condition : (1) using notify in extreme situations can cause a thread “livelock” (a false‑dead state), and (2) Condition provides higher performance.
The article first presents a simple producer‑consumer model implemented with wait and notify . With one producer and one consumer the program runs correctly, as shown by the following code:
class Factory {
private int[] items = new int[1]; // storage container (capacity 1 for demo)
private int size = 0; // actual stored size
public synchronized void put() throws InterruptedException {
do {
while (size == items.length) {
System.out.println(Thread.currentThread().getName() + " enters block");
this.wait();
System.out.println(Thread.currentThread().getName() + " awakened");
}
System.out.println(Thread.currentThread().getName() + " starts work");
items[0] = 1;
size++;
System.out.println(Thread.currentThread().getName() + " finishes work");
this.notify();
} while (true);
}
public synchronized void take() throws InterruptedException {
do {
while (size == 0) {
System.out.println(Thread.currentThread().getName() + " enters block (consumer)");
this.wait();
System.out.println(Thread.currentThread().getName() + " awakened (consumer)");
}
System.out.println("Consumer works~");
size--;
this.notify();
} while (true);
}
}
public class NotifyDemo {
public static void main(String[] args) {
Factory factory = new Factory();
Thread producer = new Thread(() -> {
try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); }
}, "Producer");
producer.start();
Thread consumer = new Thread(() -> {
try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); }
}, "Consumer");
consumer.start();
}
}When a second producer is added, the same wait/notify implementation leads to a thread “livelock”: the extra producer is awakened while the buffer is already full, causing the program to block indefinitely. The article illustrates this problem with screenshots of the execution flow.
To solve the issue, the article introduces Condition from java.util.concurrent . Two separate Condition objects are created—one for producers and one for consumers—so that only the appropriate side is signaled. The core implementation looks like this:
class FactoryByCondition {
private int[] items = new int[1];
private int size = 0;
private Lock lock = new ReentrantLock();
private Condition producerCondition = lock.newCondition();
private Condition consumerCondition = lock.newCondition();
public void put() throws InterruptedException {
do {
lock.lock();
while (size == items.length) {
System.out.println(Thread.currentThread().getName() + " enters block");
producerCondition.await();
System.out.println(Thread.currentThread().getName() + " awakened");
}
System.out.println(Thread.currentThread().getName() + " starts work");
items[0] = 1;
size++;
System.out.println(Thread.currentThread().getName() + " finishes work");
consumerCondition.signal();
} finally { lock.unlock(); }
} while (true);
}
public void take() throws InterruptedException {
do {
lock.lock();
while (size == 0) {
consumerCondition.await();
}
System.out.println("Consumer works~");
size--;
producerCondition.signal();
} finally { lock.unlock(); }
} while (true);
}
}
public class NotifyDemo {
public static void main(String[] args) {
FactoryByCondition factory = new FactoryByCondition();
Thread producer = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "Producer");
Thread producer2 = new Thread(() -> { try { factory.put(); } catch (InterruptedException e) { e.printStackTrace(); } }, "Producer2");
Thread consumer = new Thread(() -> { try { factory.take(); } catch (InterruptedException e) { e.printStackTrace(); } }, "Consumer");
producer.start(); producer2.start(); consumer.start();
}
}Running this version with two producers and one consumer shows a smooth alternating execution without livelock, confirming that Condition solves the problem.
The article also compares performance: replacing notify with notifyAll eliminates the livelock but wakes up all waiting threads, causing unnecessary context switches and reduced throughput. In contrast, Condition wakes only one appropriate thread (via signal ), leading to better performance.
In conclusion, the article recommends using Condition for thread communication in Java because it avoids the false‑deadlock caused by notify and provides superior performance compared with notifyAll .
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
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.