3. Condition Variables, Waiting & Signaling

How each language lets a thread/coroutine wait for a condition to become true and lets another wake it up -- from Java's wait/notify to Go's near-total avoidance of condition variables in favor of channels.

A condition variable answers one question: 'block me until some other thread tells me the state I care about has changed.' Java and Python have an explicit, named primitive for this. Kotlin can use Java's, but coroutine code more often reaches for a Channel instead. Go's standard library technically has sync.Cond, but idiomatic Go almost always models this with a channel instead. JavaScript has no primitive for it at all, because callbacks and Promises already are the 'notify me when ready' mechanism.

See roadmap: Condition Variables & Monitors

Language Verdict: Pros, Cons & Recommendation

Go

4/5
  • close(channel) gives a unique, simple 'notify everyone forever' primitive no other language here has directly
  • Channel-first culture avoids most of the classic condition-variable bug classes by default
  • sync.Cond, when you do need it, carries the exact same manual while-loop discipline burden as Java/Python
  • No named multiple-condition support on one lock, unlike Java's Condition

Java

4/5
  • Two complete APIs (Object monitor, Lock+Condition) covering simple and advanced needs
  • Condition supports multiple named wait-sets on one lock (notEmpty/notFull), which a plain object monitor can't do
  • Both APIs require correct while-loop discipline and manual notify-vs-notifyAll judgment -- nothing is designed away
  • Easy, classic bug source: bare if instead of while around wait()

Kotlin

4/5
  • Channel design-away's spurious wakeups and notify-target ambiguity entirely for the common producer/consumer case
  • Full Java interop when a literal condition variable is genuinely the right tool
  • No coroutine-native condition variable of its own -- must block a thread via Java interop if you need one
  • Two mental models (Java wait/notify vs. Channel) to know when to reach for which

Python

4/5
  • notify(n=...) gives finer-grained control than the other languages' binary one-or-all choice
  • queue.Queue (Condition-based) covers the common producer/consumer case without hand-rolling wait loops
  • Same manual while-loop and notify-target discipline burden as Java
  • GIL means the perceived benefit is smaller, but the same correctness rules still fully apply

JavaScript

3/5
  • No condition-variable bug class exists at all for the common single-threaded case -- Promises structurally replace it
  • Very little boilerplate needed to implement a 'wait for data' pattern by hand
  • No built-in primitive at all -- notify-one vs. notify-all, and any queueing discipline, is entirely hand-rolled
  • Zero support once real threads (workers) are involved -- Atomics.wait/notify is low-level and rarely used directly
Recommendation: Reach for the language's built-in condition variable (wait/notify+Condition, Condition, sync.Cond) only when a channel/Promise-shaped primitive genuinely doesn't fit; when it's available, prefer Channel (Kotlin/Go) or Promise (JS) since they eliminate the spurious-wakeup and notify-target bug classes by construction.

Concurrency Mechanics, Side by Side

wait / notify or the Idiomatic Equivalent

Must-know

Go and JavaScript are the interesting cases here -- both CAN express this, but neither considers a literal condition variable idiomatic.

// sync.Cond exists but is rarely reached for. The idiomatic Go // version almost always uses a channel instead, exactly like Kotlin: ch := make(chan Item, 100) go func() { ch <- item }() // producer item := <-ch // consumer -- blocks until an item exists // sync.Cond, for the rare case a channel doesn't fit: c := sync.NewCond(&mu) c.L.Lock() for queue.Empty() { c.Wait() } c.L.Unlock()

The Go standard library's own package documentation for sync.Cond notes that most code needing this pattern is better served by channels. sync.Cond is a genuine, working condition variable (Wait/Signal/Broadcast, same happens-before guarantees as Java's) but you will see it far less often in real Go code than a channel doing the same job more simply -- it tends to only appear when you're coordinating around a shared data structure a channel can't cleanly represent.

Guarded Wait Loops & Spurious Wakeups

Recommended
for queue.Empty() { // loop, same reasoning as Java, when using sync.Cond c.Wait() } // A channel receive has the same structural safety as Kotlin's: item := <-ch // never returns a zero-value 'spuriously' due to signaling

sync.Cond.Wait() carries the exact same spurious-wakeup and multiple-waiters caveat as Java's wait() -- the Go docs explicitly document that Wait() may return due to Signal or Broadcast waking multiple goroutines, so callers must loop. A channel receive has no such caveat for the same structural reason as Kotlin's Channel.

Signaling One Waiter vs. All

Recommended
c.Signal() // wakes exactly one goroutine waiting on the Cond c.Broadcast() // wakes every goroutine waiting on the Cond // A close() on a channel is Go's 'wake everyone, forever' primitive: close(done) // every current AND future receive on 'done' returns immediately

sync.Cond's Signal/Broadcast maps directly onto Java's notify/notifyAll, with the same caveat about only using Signal when waiters are interchangeable. The channel idiom for 'tell everyone, including anyone who asks later' is closing the channel -- every past, current, and future receive on a closed channel returns immediately (the zero value, plus false for the two-value receive form), which has no direct equivalent in the other four languages' primitives.

When There's No Condition Variable: the Idiomatic Alternative

Must-know

Two of the five languages here treat a literal condition variable as a last resort rather than a first choice, and it's worth being explicit about why in an interview:

  • Go: sync.Cond exists and works, but idiomatic Go reaches for a channel first, almost every time. A channel already bundles 'the data' and 'the signal that data is ready' into one operation (<-ch blocks until there's something to receive), so there's no separate condition to check-and-wait-on at all. sync.Cond mainly shows up when coordinating access to a shared data structure that a channel can't naturally represent (e.g., 'wait until this counter drops below N').
  • JavaScript: has no condition variable and no threads to need one for in the common case. The role is played structurally by Promises/callbacks — asking to be notified when something is ready IS creating (or awaiting) a Promise; there's no separate 'wait' step to write.
  • Kotlin sits in between: it can use Java's wait/notify/Condition via interop, but coroutine-native code almost always prefers a Channel for exactly the same structural reason as Go — receive() already is 'wait for the condition, then take the value' in one suspending call.

The interview-relevant takeaway: 'this language has no condition variable' should prompt 'so what does it use instead', not 'so this isn't possible' — channels and Promises are not workarounds, they are the more modern, safer replacement for the same underlying need (with the classic spurious-wakeup and notify-vs-notifyAll bugs designed away structurally rather than left as caller discipline).

Further Reading