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.
Language Verdict: Pros, Cons & Recommendation
- 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
- 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()
- 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
- 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
- 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
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.
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.
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.
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).