Concurrency is fundamental to building responsive and scalable backend systems. While modern Java offers robust concurrency abstractions via the java.util.concurrent
package, there are still times when low-level primitives like wait()
, notify()
, and notifyAll()
β provided by java.lang.Object
β are needed for fine-grained control.
In this post, weβll break down how notify()
and notifyAll()
work, when to use them, and walk through a real-world, thread-safe producer-consumer example that demonstrates the nuances of choosing between the two.
π Thread Communication via Object Monitors
Javaβs intrinsic locking is built around monitor-based synchronization. Every object has an associated monitor, and threads can only call wait()
, notify()
, or notifyAll()
inside a synchronized block or method.
synchronized (sharedObject) {
sharedObject.wait(); // releases the lock and waits
sharedObject.notify(); // wakes one waiting thread
sharedObject.notifyAll(); // wakes all waiting threads
}
When notify()
or notifyAll()
is called, it doesn't immediately wake up a thread. It moves the thread from the waiting queue to the blocked state, where it must re-acquire the lock to proceed.
π§ Choosing Between notify()
and notifyAll()
Feature | notify() |
notifyAll() |
---|---|---|
Wakes up | One waiting thread | All waiting threads |
Performance | Lower contention, better for performance | Higher contention, potential overhead |
Safety | Risky if multiple conditions exist | Safer when threads wait on different conditions |
Best for | Single condition waiters | Multiple condition types waiters (e.g., producers and consumers) |
β Rule of thumb: If you're unsure, use
notifyAll()
β it's safer. Optimize later if contention becomes a problem.
π Real-World Example: Bounded Blocking Queue (Producer-Consumer)
Letβs build a thread-safe, blocking queue where:
- Producers wait if the queue is full.
- Consumers wait if the queue is empty.
β Requirements
- Max size is bounded.
- Support for concurrent producers and consumers.
- Correct usage of
wait()
,notifyAll()
. - Clean, idiomatic code structure.
π§ Implementation
public class BlockingQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
public void enqueue(T item) throws InterruptedException {
synchronized (this) {
while (queue.size() == capacity) {
wait(); // wait until space is available
}
queue.add(item);
notifyAll(); // notify consumers waiting for items
}
}
public T dequeue() throws InterruptedException {
synchronized (this) {
while (queue.isEmpty()) {
wait(); // wait until items are available
}
T item = queue.remove();
notifyAll(); // notify producers waiting for space
return item;
}
}
public synchronized int size() {
return queue.size();
}
}
π§ͺ Usage Demo
public class ProducerConsumerDemo {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new BlockingQueue<>(5);
Runnable producer = () -> {
for (int i = 0; i < 10; i++) {
try {
queue.enqueue(i);
System.out.println("Produced: " + i);
Thread.sleep(100); // simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
Runnable consumer = () -> {
for (int i = 0; i < 10; i++) {
try {
int item = queue.dequeue();
System.out.println("Consumed: " + item);
Thread.sleep(150); // simulate processing
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
new Thread(producer).start();
new Thread(consumer).start();
}
}
β οΈ Common Pitfalls to Avoid
-
Not using a loop for
wait()
if (queue.isEmpty()) wait(); // β Wrong!
Use:
while (queue.isEmpty()) wait(); // β
Correct
-
Calling
wait()
ornotify()
outside synchronized block
queue.wait(); // β IllegalMonitorStateException
- Using
notify()
when multiple conditions exist
- If both producers and consumers wait, always use
notifyAll()
, or risk deadlock if the wrong thread is woken.
π When to Use Higher-Level Concurrency Tools
The wait()
/notify()
mechanism is powerful but error-prone. In production systems, prefer:
Use Case | Better Alternative |
---|---|
Producer-consumer |
BlockingQueue , LinkedBlockingQueue
|
Multiple conditions |
Lock + Condition
|
One-time events |
CountDownLatch , CyclicBarrier
|
Coordination of threads |
Semaphore , Phaser , CompletableFuture
|
π§Ύ Summary
- Use
notify()
to wake a single thread when only one condition exists. - Use
notifyAll()
when multiple threads might be waiting on different conditions. - Always guard
wait()
in a loop to handle spurious wakeups. - Avoid low-level constructs unless you need fine-grained control or educational insight.
In modern enterprise-grade Java systems, your default should be to reach for
java.util.concurrent
unless you have a compelling reason otherwise.
π¦ Bonus: Alternative with ReentrantLock
and Condition
Would you like a version of the above example refactored using Lock
and Condition
? It's more flexible, production-grade, and better for systems with multiple wait conditions (e.g., bounded resource pools).
Let me know and Iβll provide that next.
Top comments (0)