Loading learning content...
Mutexes answer "Who can access this right now?" Semaphores answer "How many can access this?" But neither directly answers a more nuanced question: "Should I proceed, or should I wait for a specific condition to become true?"
Consider a bounded queue: a consumer thread must wait not just for exclusive access, but specifically for the queue to become non-empty. A producer must wait for the queue to become non-full. Simple locks don't express these semantics—you'd need to release the lock, sleep, re-acquire, and check again in a polling loop.
Monitors provide the missing abstraction. They combine mutual exclusion (like a mutex) with condition variables that allow threads to efficiently wait for application-specific conditions and be precisely notified when those conditions might have changed.
By completing this page, you will understand the monitor abstraction, master the wait/notify protocol, implement condition-based coordination patterns, distinguish between different monitor semantics (Mesa vs Hoare), and apply monitors to real-world synchronization challenges.
A monitor is a synchronization construct that encapsulates:
Monitors were introduced by Per Brinch Hansen (1973) and C.A.R. Hoare (1974) as a structured approach to synchronization—safer and more maintainable than raw semaphores.
The Monitor Invariant:
At any moment, at most one thread can be "inside" the monitor—executing any of its methods. This automatic mutual exclusion means you don't explicitly acquire/release locks; the monitor handles it.
123456789101112131415161718192021222324252627282930313233
// Conceptual monitor structuremonitor BoundedBuffer { // Shared state (automatically protected) private Item[] buffer; private int count, head, tail; // Condition variables for coordination condition notFull; // Producers wait here condition notEmpty; // Consumers wait here // All methods have implicit mutual exclusion public void put(Item item) { // Only one thread executes monitor code at a time while (count == buffer.length) { wait(notFull); // Release lock, wait, re-acquire } buffer[tail] = item; tail = (tail + 1) % buffer.length; count++; signal(notEmpty); // Wake a waiting consumer } public Item take() { while (count == 0) { wait(notEmpty); // Release lock, wait, re-acquire } Item item = buffer[head]; head = (head + 1) % buffer.length; count--; signal(notFull); // Wake a waiting producer return item; }}Key Insight: The Wait Operation
The wait() operation is profound in its simplicity:
Without this atomicity, there would be a race condition: another thread could signal between our lock release and wait, causing the signal to be lost (a "lost wakeup" bug).
Never use 'if' with wait()—always use 'while'. Between receiving a signal and re-acquiring the lock, another thread might have changed the condition. This is called a 'spurious wakeup' in some contexts, though the real issue is the Mesa monitor semantics most languages use (explained later).
A condition variable is a queue of threads waiting for a specific condition to become true. It supports three operations:
Condition variables are useless without an associated lock—they're designed to work together.
| Operation | Semantics | Thread State | Lock State |
|---|---|---|---|
| wait() | Release lock, sleep, re-acquire | Running → Waiting → Ready → Running | Held → Released → Re-acquired |
| notify() | Wake one waiter (if any) | No change (signaler keeps running) | Still held by signaler |
| notifyAll() | Wake all waiters | No change (signaler keeps running) | Still held by signaler |
When to Use notify() vs notifyAll():
| Scenario | Recommendation |
|---|---|
| Only one waiter can proceed | notify() — avoids thundering herd |
| All waiters might proceed | notifyAll() — don't miss anyone |
| Different wait conditions share one CV | notifyAll() — notify() might wake wrong one |
| Worried about correctness | notifyAll() — always correct, sometimes inefficient |
Rule of thumb: When in doubt, use notifyAll(). It's less efficient but never causes lost wakeups. Use notify() only when you've proven it's correct and profiled that efficiency matters.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
import java.util.concurrent.locks.*; class BoundedBlockingQueue<T> { private final Object[] items; private int head, tail, count; // One lock, two condition variables private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public BoundedBlockingQueue(int capacity) { items = new Object[capacity]; } public void put(T item) throws InterruptedException { lock.lock(); try { // MUST be while, not if! while (count == items.length) { notFull.await(); // Wait until space available } items[tail] = item; tail = (tail + 1) % items.length; count++; notEmpty.signal(); // Wake one consumer } finally { lock.unlock(); } } @SuppressWarnings("unchecked") public T take() throws InterruptedException { lock.lock(); try { while (count == 0) { notEmpty.await(); // Wait until item available } T item = (T) items[head]; items[head] = null; // Help GC head = (head + 1) % items.length; count--; notFull.signal(); // Wake one producer return item; } finally { lock.unlock(); } } // Optional: non-blocking variants public T poll() { lock.lock(); try { if (count == 0) return null; // Same logic as take() without waiting T item = (T) items[head]; items[head] = null; head = (head + 1) % items.length; count--; notFull.signal(); return item; } finally { lock.unlock(); } }}There are two major styles of monitor semantics, named after their origins. Understanding the difference explains why while loops are mandatory in most languages.
Hoare Monitors (Signal-and-Wait)
When thread A signals, A immediately suspends and the awakened thread B runs inside the monitor. The condition is guaranteed to be true when B resumes—no re-checking needed.
Mesa Monitors (Signal-and-Continue)
When thread A signals, A keeps running. Thread B is moved to the ready queue but doesn't run until A eventually releases the lock. By then, another thread C might have changed the condition—so B must re-check.
if is sufficient (but while is safer)while is MANDATORYUsing 'if' instead of 'while' with Mesa monitors causes subtle bugs. The condition was true when signaled, but by the time the waiting thread runs, anything could have happened. This is not just about 'spurious wakeups'—it's fundamental to how Mesa monitors work. Always use while.
Why Mesa Won:
Mesa semantics dominate because:
The downside—mandatory re-checking—is trivial compared to implementation complexity of Hoare semantics.
123456789101112131415161718192021222324252627282930313233343536
// DANGEROUS: Using if with Mesa semantics class BrokenQueue { private Queue<Item> items = new LinkedList<>(); synchronized void put(Item item) { items.add(item); notify(); // Wake a consumer } synchronized Item take() { // BUG: Using 'if' instead of 'while' if (items.isEmpty()) { wait(); // Mesa: when we wake, items might be empty again! } return items.remove(); // NullPointerException! }} // CORRECT: Using whileclass CorrectQueue { private Queue<Item> items = new LinkedList<>(); synchronized void put(Item item) { items.add(item); notify(); } synchronized Item take() { // CORRECT: Always re-check condition while (items.isEmpty()) { wait(); } return items.remove(); // Safe }}Java builds monitors into every object. The synchronized keyword and Object.wait()/notify() methods provide monitor functionality without explicit lock objects.
Every Java object has:
This is convenient but limiting—you get only one condition per object. For multiple conditions, use java.util.concurrent.locks (shown earlier).
12345678910111213141516171819202122232425262728293031323334353637383940
class BoundedQueue<T> { private final Object[] items; private int head, tail, count; public BoundedQueue(int capacity) { items = new Object[capacity]; } // synchronized = auto lock on 'this' public synchronized void put(T item) throws InterruptedException { while (count == items.length) { wait(); // this.wait() - uses 'this' as monitor } items[tail] = item; tail = (tail + 1) % items.length; count++; notifyAll(); // this.notifyAll() - only one condition! } @SuppressWarnings("unchecked") public synchronized T take() throws InterruptedException { while (count == 0) { wait(); } T item = (T) items[head]; items[head] = null; head = (head + 1) % items.length; count--; notifyAll(); // Must wake ALL since one condition for both return item; } public synchronized int size() { return count; }}Never synchronize on 'this' if your class is public, and never synchronize on publicly accessible objects. External code could synchronize on the same object, causing unexpected contention or deadlock. Use a private lock object for encapsulation.
Monitors enable sophisticated coordination patterns beyond simple producer-consumer. Here are patterns you'll encounter in complex systems:
Barrier (Rendezvous Point)
N threads must all reach a point before any can proceed. Used for phased algorithms where each phase must complete before the next begins.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
class Barrier { private final int parties; private int waiting = 0; private int generation = 0; // Reuse barrier public Barrier(int parties) { this.parties = parties; } public synchronized void await() throws InterruptedException { int myGeneration = generation; waiting++; if (waiting == parties) { // Last thread to arrive waiting = 0; generation++; // Reset for reuse notifyAll(); // Release everyone } else { // Wait until generation changes while (myGeneration == generation) { wait(); } } }} // Usage: Parallel computation with sync pointsvoid parallelSort(int[] data, int threadCount) { Barrier barrier = new Barrier(threadCount); for (int t = 0; t < threadCount; t++) { final int threadId = t; new Thread(() -> { // Phase 1: Local sort sortLocalChunk(data, threadId); barrier.await(); // Wait for all chunks sorted // Phase 2: Merge mergeWithNeighbor(data, threadId); barrier.await(); // Wait for all merges // Phase 3: Verify verifyOrder(data, threadId); }).start(); }}Monitors are safer than raw semaphores but still have traps for the unwary:
1234567891011121314151617181920212223242526272829303132
// LOST WAKEUP BUG class BrokenSignaling { private boolean signaled = false; // BAD: Lost wakeup possible synchronized void waitForSignal() throws InterruptedException { wait(); // If signal came before, we wait forever } synchronized void sendSignal() { notify(); }} // CORRECT: State-based signaling class CorrectSignaling { private boolean signaled = false; synchronized void waitForSignal() throws InterruptedException { while (!signaled) { // Check state, not event wait(); } signaled = false; // Reset if needed } synchronized void sendSignal() { signaled = true; // Update state notify(); }}We've explored monitors from concept to advanced patterns. Here are the key takeaways:
What's Next:
We've now covered the three fundamental synchronization primitives: mutexes, semaphores, and monitors. The final page addresses the crucial question of choosing the right synchronization mechanism—a decision framework for selecting the appropriate primitive based on your specific coordination requirements.
You now understand monitors—the high-level synchronization construct that powers most concurrent programming. You can implement complex coordination patterns like barriers, latches, and read-write locks using condition variables. Next, we'll synthesize everything into a decision framework for choosing synchronization mechanisms.