Condition Variables in Concurrent Programming
In multithreaded applications, mutual exclusion—ensuring only one thread accesses a resource at a time—is often insufficient. Many scenarios require a thread to wait until a specific condition (a predicate) becomes true before it can proceed. Without a proper mechanism, developers often resort to "busy waiting" or "spin-waiting," where a thread repeatedly checks a condition in a loop. This is inefficient: it either hogs the CPU or introduces latency and unresponsiveness.
The solution to this problem is the condition variable. A condition variable is essentially a queue of threads associated with a mutex, allowing threads to sleep efficiently until they are signaled that a specific state has been reached.
Key Facts
- Condition Variables allow threads to block (sleep) until a specific condition is met, preventing CPU waste.
- Wait Operation atomically releases the associated mutex and puts the thread to sleep.
- Signal/Notify wakes up one thread from the wait queue; Broadcast/NotifyAll wakes up all waiting threads.
- Hoare-style monitors use "signal and urgent wait," where the signaler immediately yields the lock to the signaled thread.
- Mesa-style monitors use "signal and continue," where the signaled thread is moved to the ready queue and must re-verify the condition.
The Bounded Producer-Consumer Problem
To understand why condition variables are necessary, consider the classic bounded producer-consumer problem. In this scenario, multiple "producer" threads add tasks to a fixed-size queue, and "consumer" threads remove them. The queue is not thread-safe, meaning concurrent access can lead to race conditions and corrupted data.
Two primary constraints exist:
- Producers must block if the queue is full.
- Consumers must block if the queue is empty.
If these threads use simple spin-locks, they waste immense CPU resources checking the queue status. If they don't use synchronization at all, they may attempt to dequeue from an empty queue or enqueue into a full one, leading to system errors.
How Condition Variables Work
A condition variable c is associated with an assertion P. When a thread finds that P is false, it calls a wait operation. This operation is critical because it must be atomic to avoid "missed wakeups," where a signal is sent after the mutex is released but before the thread actually falls asleep.
Core Operations
- wait(c, m): Atomically releases mutex m, moves the thread to the wait-queue of c, and puts the thread to sleep. Upon waking, the thread automatically re-acquires the mutex.
- signal(c) / notify(c): Wakes up one thread from the wait-queue. It is generally best practice to signal before releasing the mutex, though implementation details vary.
- broadcast(c) / notifyAll(c): Wakes all threads in the wait-queue. This is necessary when multiple different conditions are tied to one variable, as signaling a single thread might wake one that cannot yet proceed, leaving others who could proceed still asleep.
A key design rule is that multiple condition variables can share one mutex, but a single condition variable cannot be associated with multiple mutexes. In the producer-consumer example, one mutex protects the queue, while two condition variables (c_full and c_empty) manage the different waiting states.
Monitor Implementation Styles
Different systems implement the interaction between signaling and lock ownership differently. These are generally categorized as Hoare-style and Mesa-style monitors.
Hoare-Style (Blocking) Monitors
Also known as "signal and urgent wait," these monitors ensure that when a thread is signaled, it immediately gains occupancy of the monitor. The signaling thread is forced to wait until the signaled thread either finishes or waits on another condition. This guarantees that the condition P remains true the moment the waiting thread wakes up.

Mesa-Style (Non-blocking) Monitors
In "signal and continue" monitors, signaling a thread does not force the signaler to give up the lock. Instead, the signaled thread is moved to the entrance queue. Because other threads might enter the monitor and change the state before the signaled thread gets the lock, the condition P may no longer be true when the thread wakes up.

Because of this, Mesa-style monitors require that wait operations be enclosed in a while-loop. The thread must re-check the condition upon waking to ensure it is still valid.
Implicit Monitors
Some languages, such as Java or C#, use implicit monitors. In these systems, every object has a single built-in wait queue. All wait and notifyAll operations apply to this single queue rather than explicit, named condition variables.

Comparison of Monitor Types
| Feature | Hoare-Style | Mesa-Style | Implicit (e.g., Java) |
|---|---|---|---|
| Signaling Logic | Signal and Urgent Wait | Signal and Continue | Signal and Continue |
| Lock Transfer | Immediate transfer to signaled thread | Signaled thread enters ready queue | Signaled thread enters ready queue |
| Wait Pattern | if (condition) wait(); |
while (condition) wait(); |
while (condition) wait(); |
| Condition Variables | Explicit and multiple | Explicit and multiple | Single implicit queue per object |
Frequently Asked Questions
Why is a while-loop necessary for Mesa-style monitors?
In Mesa-style monitors, the signaled thread does not get the lock immediately. Other threads may acquire the lock and change the state of the resource before the signaled thread wakes up. The loop ensures the thread re-verifies the condition before proceeding.
What is a "missed wakeup"?
A missed wakeup occurs if the release of the mutex and the act of going to sleep are not atomic. If a signal is sent after the mutex is released but before the thread is officially "asleep," the signal is lost, and the thread may sleep indefinitely.
When should I use broadcast instead of signal?
Use broadcast (or notifyAll) when multiple threads are waiting on different conditions using the same condition variable. Using a single signal might wake a thread whose specific condition is still false, while a thread whose condition is true remains asleep.
Can multiple condition variables use the same mutex?
Yes. In fact, this is common. For example, in a bounded buffer, one mutex protects the buffer's integrity, while one condition variable manages the "full" state and another manages the "empty" state.
What is the difference between a mutex and a condition variable?
A mutex provides mutual exclusion (preventing simultaneous access), whereas a condition variable provides a mechanism for threads to synchronize based on the actual state of the data (waiting for a specific condition to become true).