Monitors & Condition Variables
IntermediateA monitor combines a mutex and condition variables into a single high-level construct; threads call wait() to release the lock and sleep, and notify() to wake sleeping threads.
Overview
A monitor is a higher-level synchronization abstraction that encapsulates shared data, the mutex protecting it, and condition variables for coordination. Every Java object is implicitly a monitor: the synchronized keyword acquires the object's intrinsic lock (mutex), and Object.wait()/notify()/notifyAll() are the condition variable operations. wait() atomically releases the lock and suspends the thread on a wait set. notify() moves one thread from the wait set back to the lock queue. notifyAll() moves all. Because spurious wakeups can occur (a thread may be woken without notify() being called), the correct pattern is always to check the wait condition in a while loop, never an if. Java 5+ ReentrantLock with Condition objects provides explicit, named conditions — more flexible than the intrinsic monitor for complex synchronization patterns.
Java Synchronized + wait/notifyAll: Correct Monitor Pattern
The monitor pattern in Java: synchronized method or block acquires the intrinsic lock. wait() releases the lock and parks the thread. notify()/notifyAll() wakes waiting threads. The woken thread must re-acquire the lock before returning from wait(). The while-loop pattern is mandatory: spurious wakeups make if-based checks unsafe.
// Correct monitor pattern: while loop around wait()
class BoundedBuffer<T> {
private final Queue<T> buffer = new LinkedList<>();
private final int capacity;
BoundedBuffer(int capacity) { this.capacity = capacity; }
// Producer: put item, wait if buffer is full
public synchronized void put(T item) throws InterruptedException {
while (buffer.size() == capacity) { // MUST be while, not if!
wait(); // releases lock, parks thread — avoids CPU spinning
}
buffer.add(item);
notifyAll(); // wake all waiting consumers (and other producers)
System.out.println("Produced: " + item + " buffer=" + buffer.size());
}
// Consumer: take item, wait if buffer is empty
public synchronized T take() throws InterruptedException {
while (buffer.isEmpty()) { // MUST be while — spurious wakeup protection
wait();
}
T item = buffer.poll();
notifyAll(); // wake all waiting producers
System.out.println("Consumed: " + item + " buffer=" + buffer.size());
return item;
}
}
BoundedBuffer<Integer> buf = new BoundedBuffer<>(3);
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { buf.put(i); } catch (InterruptedException e) { break; }
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { buf.take(); } catch (InterruptedException e) { break; }
}
});
producer.start(); consumer.start();
producer.join(); consumer.join();ReentrantLock + Condition: Named Conditions
The intrinsic monitor has a single wait set — notifyAll() wakes all threads even if most conditions are not satisfied. ReentrantLock with multiple Condition objects fixes this: notFull.signal() wakes only producers; notEmpty.signal() wakes only consumers. This eliminates spurious wakeups between producers and consumers and improves throughput.
// ReentrantLock with two named Condition variables — eliminates cross-wakeups
class OptimalBuffer<T> {
private final Queue<T> queue = new ArrayDeque<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // producers wait here
private final Condition notEmpty = lock.newCondition(); // consumers wait here
OptimalBuffer(int capacity) { this.capacity = capacity; }
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) notFull.await(); // only producers wait here
queue.add(item);
notEmpty.signal(); // wake exactly ONE consumer — no thundering herd
} finally { lock.unlock(); }
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await(); // only consumers wait here
T item = queue.poll();
notFull.signal(); // wake exactly ONE producer
return item;
} finally { lock.unlock(); }
}
}
// Spurious wakeup: JVM may wake a thread from wait() without notify() being called
// This is allowed by the Java Memory Model specification
// ALWAYS use while loop — never if — around wait() calls
// while (condition not met) { lock.wait(); } ← CORRECT
// if (condition not met) { lock.wait(); } ← BUG: proceeds without condition being trueKey Points to Remember
- 1Monitor = mutex + condition variable + shared data combined in one construct.
- 2Java object intrinsic lock: synchronized acquires it; wait() releases it and parks; notify()/notifyAll() wakes.
- 3Spurious wakeups are permitted by the Java spec — always use while loop, never if, around wait().
- 4notifyAll() wakes all threads from the wait set; notify() wakes one (nondeterministically).
- 5ReentrantLock + Condition allows multiple named wait sets — avoids waking unrelated threads.
- 6Always acquire lock in try and release in finally to prevent deadlock on exception.
Interview Questions
Sign in to ask AriaWhat is a spurious wakeup and why must wait() always be called in a while loop?
What is the difference between notify() and notifyAll()?
How do ReentrantLock Condition objects improve on the intrinsic monitor?
Implement a thread-safe bounded buffer using synchronized + wait/notifyAll.
Ask Aria about Monitors & Condition Variables
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.