Let’s compare wait()/notify()
(used with synchronized
) and Condition
(used with ReentrantLock
) — both are used for thread coordination, but Condition
gives you more flexibility.
We’ll walk through:
- A
wait()/notify()
example - A
Condition
example doing the same thing
Scenario: We have a producer and consumer. The producer sets a flag when data is ready; the consumer waits until that happens.
🧵 1. wait()/notify()
Example with synchronized
class SharedData {
private boolean ready = false;
public synchronized void waitForData() {
while (!ready) {
try {
wait(); // 💤 releases monitor and waits
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Consumer: data is ready!");
}
public synchronized void produceData() {
ready = true;
notify(); // 🚨 wakes one waiting thread
System.out.println("Producer: data produced!");
}
}
public class Main {
public static void main(String[] args) {
SharedData data = new SharedData();
Thread consumer = new Thread(data::waitForData);
Thread producer = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
data.produceData();
});
consumer.start();
producer.start();
}
}
✅ Output:
Producer: data produced!
Consumer: data is ready!
🔐 2. Same Logic Using Condition
+ ReentrantLock
import java.util.concurrent.locks.*;
class SharedDataWithLock {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;
public void waitForData() {
lock.lock();
try {
while (!ready) {
condition.await(); // 💤 releases lock and waits
}
System.out.println("Consumer: data is ready!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public void produceData() {
lock.lock();
try {
ready = true;
condition.signal(); // 🚨 wakes one waiting thread
System.out.println("Producer: data produced!");
} finally {
lock.unlock();
}
}
}
public class Main {
public static void main(String[] args) {
SharedDataWithLock data = new SharedDataWithLock();
Thread consumer = new Thread(data::waitForData);
Thread producer = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
data.produceData();
});
consumer.start();
producer.start();
}
}
✅ Output is the same, but…
🧠 Why Use Condition
Over wait()/notify()
?
Feature | wait()/notify() | Condition (with Lock) |
---|---|---|
Number of condition queues | 1 per monitor | ✅ Multiple possible |
Targeted signaling | ❌ No (notify() ) | ✅ Yes (condition.signal() ) |
Locking flexibility | Only intrinsic locks (synchronized ) | ✅ Any Lock (like ReentrantLock ) |
Reentrant support | ✅ Yes | ✅ Yes |
Fairness, timeout control | Limited | ✅ Full support |
🧩 Summary
- Use
wait()/notify()
for simple coordination inside synchronized blocks - Use
Condition
if you need:- Multiple wait conditions
- Better signaling control
- Integration with
ReentrantLock
,tryLock
, or advanced behavior