
Java Thread Wait, Notify, and NotifyAll Example
Java’s wait(), notify(), and notifyAll() methods form the foundation of thread synchronization in multi-threaded applications. These methods, inherited from the Object class, enable threads to communicate and coordinate their execution by allowing one thread to pause and wait for a condition while another thread signals when that condition is met. Understanding these mechanisms is crucial for building robust concurrent applications, preventing race conditions, and implementing efficient producer-consumer patterns. This guide will walk you through practical implementations, common pitfalls, and real-world scenarios where these synchronization primitives shine.
How Java Thread Synchronization Works
The wait-notify mechanism operates on the concept of intrinsic locks (monitors) that every Java object possesses. When a thread calls wait() on an object, it releases the object’s lock and enters a waiting state until another thread calls notify() or notifyAll() on the same object.
Here’s the fundamental flow:
- wait() – Causes the current thread to wait until another thread invokes notify() or notifyAll() for this object
- notify() – Wakes up a single thread that is waiting on this object’s monitor
- notifyAll() – Wakes up all threads that are waiting on this object’s monitor
All three methods must be called from within a synchronized block or method, otherwise you’ll get an IllegalMonitorStateException. The calling thread must own the object’s monitor.
public class BasicSynchronization {
private final Object lock = new Object();
private boolean condition = false;
public void waitingMethod() throws InterruptedException {
synchronized (lock) {
while (!condition) {
System.out.println("Thread " + Thread.currentThread().getName() + " is waiting...");
lock.wait(); // Releases lock and waits
}
System.out.println("Thread " + Thread.currentThread().getName() + " resumed execution");
}
}
public void notifyingMethod() {
synchronized (lock) {
condition = true;
System.out.println("Condition set to true by " + Thread.currentThread().getName());
lock.notify(); // or lock.notifyAll()
}
}
}
Step-by-Step Implementation Guide
Let’s build a complete producer-consumer example that demonstrates proper usage of wait() and notify() methods:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
private final Object lock = new Object();
// Producer class
class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
try {
produce(value++);
Thread.sleep(1000); // Simulate production time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private void produce(int value) throws InterruptedException {
synchronized (lock) {
// Wait while queue is full
while (queue.size() == CAPACITY) {
System.out.println("Queue is full, producer waiting...");
lock.wait();
}
queue.offer(value);
System.out.println("Produced: " + value + " | Queue size: " + queue.size());
// Notify waiting consumers
lock.notifyAll();
}
}
}
// Consumer class
class Consumer implements Runnable {
private final String name;
Consumer(String name) {
this.name = name;
}
@Override
public void run() {
while (true) {
try {
consume();
Thread.sleep(2000); // Simulate consumption time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
private void consume() throws InterruptedException {
synchronized (lock) {
// Wait while queue is empty
while (queue.isEmpty()) {
System.out.println(name + " waiting for items...");
lock.wait();
}
int value = queue.poll();
System.out.println(name + " consumed: " + value + " | Queue size: " + queue.size());
// Notify waiting producers
lock.notifyAll();
}
}
}
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Thread producer = new Thread(example.new Producer());
Thread consumer1 = new Thread(example.new Consumer("Consumer-1"));
Thread consumer2 = new Thread(example.new Consumer("Consumer-2"));
producer.start();
consumer1.start();
consumer2.start();
}
}
Real-World Examples and Use Cases
Here are practical scenarios where wait-notify mechanisms prove invaluable:
Database Connection Pool
public class ConnectionPool {
private final Queue<Connection> availableConnections = new LinkedList<>();
private final Set<Connection> usedConnections = new HashSet<>();
private final int maxPoolSize;
private final Object poolLock = new Object();
public ConnectionPool(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
// Initialize pool with connections
initializePool();
}
public Connection getConnection() throws InterruptedException {
synchronized (poolLock) {
while (availableConnections.isEmpty() &&
usedConnections.size() >= maxPoolSize) {
System.out.println("Pool exhausted, waiting for connection...");
poolLock.wait();
}
Connection connection = availableConnections.poll();
if (connection == null) {
connection = createNewConnection();
}
usedConnections.add(connection);
return connection;
}
}
public void releaseConnection(Connection connection) {
synchronized (poolLock) {
if (usedConnections.remove(connection)) {
availableConnections.offer(connection);
poolLock.notifyAll(); // Wake up waiting threads
}
}
}
private void initializePool() {
// Implementation details for creating initial connections
}
private Connection createNewConnection() {
// Implementation for creating new database connections
return null; // Placeholder
}
}
Task Scheduling System
public class TaskScheduler {
private final Queue<Runnable> taskQueue = new LinkedList<>();
private final Object queueLock = new Object();
private volatile boolean shutdown = false;
public void submitTask(Runnable task) {
synchronized (queueLock) {
if (!shutdown) {
taskQueue.offer(task);
queueLock.notify(); // Wake up worker thread
}
}
}
public void startWorker() {
Thread worker = new Thread(() -> {
while (!shutdown || !taskQueue.isEmpty()) {
Runnable task = null;
synchronized (queueLock) {
while (taskQueue.isEmpty() && !shutdown) {
try {
queueLock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (!taskQueue.isEmpty()) {
task = taskQueue.poll();
}
}
if (task != null) {
try {
task.run();
} catch (Exception e) {
System.err.println("Task execution failed: " + e.getMessage());
}
}
}
});
worker.start();
}
public void shutdown() {
synchronized (queueLock) {
shutdown = true;
queueLock.notifyAll();
}
}
}
Comparison with Alternative Approaches
Approach | Complexity | Performance | Features | Best Use Case |
---|---|---|---|---|
wait/notify | Medium | Good | Basic synchronization | Simple producer-consumer patterns |
java.util.concurrent.locks | Higher | Better | Advanced features, fairness | Complex synchronization requirements |
BlockingQueue | Low | Excellent | Thread-safe collections | Producer-consumer with bounded queues |
Semaphore | Low | Good | Resource counting | Limiting concurrent access |
CountDownLatch | Low | Excellent | One-time events | Waiting for multiple threads to complete |
Performance Considerations and Benchmarks
Here’s a simple benchmark comparing wait/notify with BlockingQueue:
public class PerformanceBenchmark {
private static final int ITEMS_TO_PROCESS = 100000;
private static final int QUEUE_CAPACITY = 1000;
public static void benchmarkWaitNotify() {
long startTime = System.nanoTime();
// Your wait/notify implementation here
ProducerConsumerExample example = new ProducerConsumerExample();
// Run benchmark...
long endTime = System.nanoTime();
System.out.println("Wait/Notify: " + (endTime - startTime) / 1_000_000 + "ms");
}
public static void benchmarkBlockingQueue() {
long startTime = System.nanoTime();
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
// Run BlockingQueue benchmark...
long endTime = System.nanoTime();
System.out.println("BlockingQueue: " + (endTime - startTime) / 1_000_000 + "ms");
}
}
Typical performance characteristics:
- wait/notify: ~15-20% overhead compared to optimized concurrent collections
- Memory usage: Lower due to simpler object structure
- Scalability: Good for moderate concurrency levels (< 100 threads)
- Latency: Can suffer from thundering herd with notifyAll()
Best Practices and Common Pitfalls
Essential Best Practices
- Always use while loops, not if statements when checking conditions before wait()
- Handle InterruptedException properly to support thread cancellation
- Use notifyAll() instead of notify() unless you’re certain only one thread should wake up
- Keep synchronized blocks as short as possible to minimize contention
- Use separate objects for different conditions to avoid unnecessary wake-ups
Common Pitfalls to Avoid
// BAD: Using if instead of while
synchronized (lock) {
if (condition) { // Wrong! Use while
lock.wait();
}
}
// GOOD: Using while loop
synchronized (lock) {
while (!condition) { // Correct approach
lock.wait();
}
}
// BAD: Not handling InterruptedException
try {
lock.wait();
} catch (InterruptedException e) {
// Ignoring exception - Wrong!
}
// GOOD: Proper exception handling
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore interrupt status
throw new RuntimeException("Thread interrupted", e);
}
// BAD: Calling wait() outside synchronized block
lock.wait(); // Throws IllegalMonitorStateException
// GOOD: Always synchronize first
synchronized (lock) {
lock.wait();
}
Advanced Patterns
public class MultiConditionWaiting {
private final Object lock = new Object();
private boolean condition1 = false;
private boolean condition2 = false;
public void waitForBothConditions() throws InterruptedException {
synchronized (lock) {
while (!(condition1 && condition2)) {
lock.wait();
}
// Both conditions are now true
performAction();
}
}
public void setCondition1() {
synchronized (lock) {
condition1 = true;
lock.notifyAll(); // Wake up threads waiting for either condition
}
}
public void setCondition2() {
synchronized (lock) {
condition2 = true;
lock.notifyAll();
}
}
private void performAction() {
System.out.println("Both conditions met, executing action...");
}
}
Integration with Modern Java Features
While wait/notify remains relevant, consider these modern alternatives for new projects:
// Using CompletableFuture for async operations
public class ModernAsyncExample {
public CompletableFuture<String> processAsync() {
return CompletableFuture.supplyAsync(() -> {
// Background processing
return "Result";
}).thenApply(result -> {
// Transform result
return result.toUpperCase();
});
}
}
// Using virtual threads (Java 19+)
public class VirtualThreadExample {
public void processWithVirtualThreads() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
// Process task
System.out.println("Processing on " + Thread.currentThread());
});
}
}
}
}
For comprehensive documentation on Java concurrency, check the official Oracle documentation and the Java Concurrency Tutorial.
Understanding wait(), notify(), and notifyAll() provides a solid foundation for Java concurrency, even as newer constructs like CompletableFuture and virtual threads reshape how we handle asynchronous programming. These fundamentals remain crucial for maintaining legacy systems and understanding the internals of higher-level concurrency utilities.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.