skip to content

Why do wait(), notify() and notifyAll() live on java.lang.Object rather than on Thread, and what rule governs calling them?

level: seniorimportance: should knowfreq 45%

answer

  1. Every object has a monitor + wait-set → methods live on Object
  2. Must hold the monitor (synchronized) or IllegalMonitorStateException
  3. wait() atomically releases lock and parks; re-acquires on return
  4. notifyAll safer than notify; woken threads re-contend for the lock
  5. Always wait in a while-loop (spurious wakeups + stale condition)

basics

~20 s

Every Java object has an intrinsic lock (monitor) and an associated wait-set, so the wait/notify methods belong on Object — the thing being locked. You must call them while holding that object's monitor (inside synchronized), or you get IllegalMonitorStateException.

solid answer

~50 s

wait(), notify() and notifyAll() are final methods on Object because the coordination primitive they drive — the wait-set — is a property of an object's intrinsic lock (monitor), and every object has one. Threads coordinate *around a shared object*, not around each other, so the methods belong on Object, the lockable thing. The governing rule: you must hold the object's monitor (be inside a synchronized block/method on that object) when calling them, or the JVM throws IllegalMonitorStateException. wait() atomically releases the monitor and parks the thread in the object's wait-set; notify() wakes one waiter, notifyAll() wakes all, but the woken threads must re-acquire the monitor before proceeding. Because of spurious wakeups and because the condition may change between notify and reacquire, wait() must always be called in a loop that re-checks the guard condition (while, not if). They are final so the fixed monitor protocol can't be altered.

go deeper

for a junior

Knows wait/notify exist for thread coordination and are on Object.

for a middle

Knows they require holding the monitor (synchronized) and that wait releases the lock; can sketch producer-consumer.

for a senior

Explains the monitor/wait-set rationale for living on Object, the guarded-loop requirement, spurious wakeups, and notify vs notifyAll safety.

for a principal

Reasons about when to drop to wait/notify vs higher-level synchronizers, lost-wakeup and missed-signal hazards, fairness, and the happens-before edges the monitor establishes.

## The core idea: monitors belong to objects In Java, **every object** has an associated **intrinsic lock**, also called a **monitor**. `synchronized(obj)` acquires `obj`'s monitor. Alongside the lock, each object also has a **wait-set**: a holding area for threads that have voluntarily paused, waiting for some condition tied to that object to become true. Threads in Java don't coordinate by talking to each other directly; they coordinate **through a shared object** — a queue, a buffer, a latch. The natural home for 'pause until something changes about *this object*' is therefore the **object itself**, which is why `wait()`, `notify()`, and `notifyAll()` are defined on `java.lang.Object` rather than on `Thread`. If they lived on `Thread`, you'd be waiting on a *thread*, which doesn't match the mental model of guarding shared state. ## The mandatory rule: hold the monitor You may call these three methods **only while holding the object's monitor** — i.e., from inside a `synchronized` block or method on that same object. Otherwise the JVM immediately throws **`IllegalMonitorStateException`** (an unchecked exception). This is required because the methods manipulate the monitor's wait-set, which is only safe under the lock. ```java synchronized (queue) { // hold queue's monitor while (queue.isEmpty()) { // guard loop, not an if queue.wait(); // releases monitor, parks in queue's wait-set } item = queue.remove(); } ``` ## What each method does - **`wait()`**: *atomically* releases the monitor and parks the current thread in the object's wait-set. Releasing the lock is essential — otherwise no other thread could change the state being waited on (deadlock). When later notified and rescheduled, the thread **re-acquires the monitor** before `wait()` returns. - **`notify()`**: wakes **one** arbitrary thread from the wait-set. The woken thread doesn't run immediately — it must re-acquire the monitor first (the notifier still holds it until it leaves the synchronized region). - **`notifyAll()`**: wakes **all** waiting threads; they then contend to re-acquire the monitor one at a time. Generally **safer** than `notify()` because `notify()` can wake the 'wrong' thread (one whose condition still isn't satisfied) and lose a wakeup. ## Why the guarded loop (`while`, not `if`) Two reasons you must re-check the condition after `wait()` returns: 1. **Spurious wakeups**: a thread can wake from `wait()` without any `notify()` — the spec explicitly permits this. 2. **Stale condition**: between `notify()` and the woken thread re-acquiring the lock, another thread may have changed the state (e.g., consumed the item). So the condition that was true at notify time may be false again. Hence the canonical pattern is `while (!condition) obj.wait();`. ## Why they're `final` The methods are `final` on `Object` so the **monitor protocol is fixed and uniform** for every object — the runtime implements wait/notify in the JVM, and allowing overrides would let a class corrupt the locking semantics that other code and the JVM depend on. ## Modern note This is the low-level primitive. Higher-level utilities (`ReentrantLock` + `Condition`, `BlockingQueue`, `CountDownLatch`, `Semaphore`) are usually preferable, but they're built on these same happens-before/monitor concepts, so understanding wait/notify remains foundational.

  • Why must wait() be called inside a loop that re-checks the condition?
    Because of spurious wakeups (a thread can wake with no notify) and because the guarded state may change between notify and the thread re-acquiring the lock. Re-checking with while ensures the thread only proceeds when the condition truly holds.
  • What's the practical difference between notify() and notifyAll()?
    notify() wakes one arbitrary waiter; if that thread's condition isn't satisfied it may re-wait, potentially stranding others (a lost wakeup). notifyAll() wakes all waiters to re-check and contend, which is safer when multiple distinct conditions share one monitor.

saying these in an interview costs you the question

  • Putting wait/notify on Thread instead of the shared object
  • Calling wait/notify outside synchronized (IllegalMonitorStateException)
  • Using if instead of while around wait() (ignores spurious wakeups)
  • Thinking wait() keeps holding the lock while parked
  • Assuming notify() immediately runs the woken thread

context