Back to the map

Module 05 · Synchronization · Lesson 4 of 6

Monitors and condition variables

How threads wait for a condition without spinning: condition variables paired with a lock, the wait-in-a-loop rule, spurious wakeups, signal versus broadcast, and a bounded buffer built from them. Plus monitors and their language forms.

CoreShould-knowMid 12 min Updated Nov 2026

Why it matters

Locks keep threads out of each other's way. Many problems also need threads to wait for each other: a consumer waits until a queue has items, a producer waits until it has space, a worker waits until shutdown or new work. Polling a flag in a loop burns CPU; sleeping a fixed time adds latency. Condition variables are the primitive under every blocking queue, thread pool and future, and their rules are where subtle hangs and lost wakeups come from.

The idea

A condition variable lets a thread sleep until another thread tells it that something it cares about may have changed. It is always used with a mutex that protects the shared state the condition is about. Its operations:

  1. 1

    wait(mutex)

    Atomically release the mutex and put the thread to sleep; when woken, reacquire the mutex before returning. The atomic release-and-sleep is the crucial part: it closes the window in which a signal could be sent after the thread checked the condition but before it slept, the lost wakeup.
  2. 2

    signal (notify)

    Wake one waiting thread, if any.
  3. 3

    broadcast (notifyAll)

    Wake every waiting thread; each re-checks its condition.

Always wait in a loop

A woken thread must re-check its condition, because the condition may no longer hold by the time it reacquires the mutex (another thread got there first), and because implementations permit spurious wakeups with no signal at all. The correct shape is always: lock; while the condition is false, wait; act; unlock. Using if instead of while is the single most common condition-variable bug.

ProducersConsumersBuffer (capacity 4)guarded by one mutexputtakenotFull: producers wait here when count == 4notEmpty: consumers wait here when count == 0put signals notEmpty · take signals notFull
A bounded buffer: producers wait while it is full, consumers wait while it is empty. One mutex protects the buffer; two condition variables carry the 'not full' and 'not empty' signals.

The bounded buffer

The classic producer-consumer queue uses one mutex and two condition variables. To put: lock; while the buffer is full, wait on notFull; add the item; signal notEmpty; unlock. To take: lock; while the buffer is empty, wait on notEmpty; remove an item; signal notFull; unlock. This is exactly what Java's ArrayBlockingQueue and Go's buffered channels do internally, and it gives backpressure for free: a slow consumer makes producers block instead of letting memory grow without limit.

Signal or broadcast

Use signal when any one waiter can make progress and all waiters wait for the same condition (one item added, one consumer needed). Use broadcast when the change may let several waiters proceed, or when waiters wait for different conditions on the same condition variable, because signal might wake one that cannot proceed while the one that could keeps sleeping. Broadcast is always correct if waiters loop, at the cost of waking threads needlessly (the thundering herd).

Monitors

A monitor packages shared data, a mutex and condition variables into one object whose methods run with the lock held. Java's synchronized methods with wait and notify on the object, and C#'s lock with Monitor.Wait and Pulse, are monitors built into the language. Most languages also offer higher-level tools built on these (blocking queues, countdown latches, barriers, futures, channels), which are almost always the better choice in application code than raw condition variables.

NeedUse
Hand work from producers to consumersA blocking queue or channel
Wait for N tasks to finishA countdown latch, WaitGroup, or join on futures
Wait for one resultA future or promise
All threads reach a point before any continuesA barrier
Custom condition over shared stateA mutex plus condition variables, waiting in a loop

Trade-offs

Condition variables avoid busy-waiting and fixed sleeps, at the cost of rules that are easy to break: hold the lock, wait in a loop, choose signal or broadcast correctly. Monitors make the pattern safer by construction. Higher-level primitives remove most of the risk and should be the default; raw condition variables belong in the implementation of those primitives or in rare custom cases.

In an interview

Use the bounded buffer to talk about backpressure: "The ingest stage hands batches to writers through a bounded queue of 1,000; when writers fall behind, the queue fills and ingest blocks, so memory stays bounded instead of growing until the process is killed." If asked to implement one, write the loop-around-wait form and say why it is a loop.

Check yourself

A consumer does: lock; if (queue.isEmpty()) wait(); item = queue.remove(); unlock. Occasionally it throws because the queue is empty after waking. Why?

Monitors and condition variables · Backplane