Fundamentals 10 min read

Why Java’s wait() Must Be Called Inside a synchronized Block

Because wait() first releases the monitor, the calling thread must own the lock; the JVM enforces this with an IllegalMonitorStateException, and the synchronized block prevents the lost‑wake‑up race by atomically checking conditions and releasing the lock before waiting.

samdeepthink
samdeepthink
samdeepthink
Why Java’s wait() Must Be Called Inside a synchronized Block

Many developers first learn the rule that wait() must be placed inside a synchronized block, but memorizing the rule and understanding its rationale are different tasks.

In Java each object header contains a monitor , an implicit lock. The synchronized keyword makes a thread compete for ownership of this monitor; only the owner can execute the protected critical section, while other threads block or spin.

The monitor protects the ordering of operations on data, not the data itself. This ordering guarantee is the prerequisite for using wait() correctly.

What wait() actually does can be broken into four steps:

Check whether the current thread holds the object's monitor; if not, throw IllegalMonitorStateException.

Place the thread into the object's waiting queue (the HotSpot WaitSet).

Release the monitor held by the thread.

Suspend the thread until it is notified, interrupted, or times out.

Step three is crucial: before the thread really sleeps, it voluntarily hands over the lock. Without this hand‑off, other threads could never enter the critical section to call notify(), creating a deadlock.

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
    CHECK_OWNER(); // throws IllegalMonitorStateException if lock not held
    // ... subsequent logic
}

The first line, CHECK_OWNER, verifies that the current thread is the monitor owner. If the check fails, the JVM throws IllegalMonitorStateException, a hard‑coded safety check that prevents code from running without the required lock.

Beyond the source‑level check, the design aims to avoid race conditions. wait() exists so a thread can pause until a certain condition becomes true. The thread must first test the condition and then wait atomically; otherwise a "lost wake‑up" can occur.

// Incorrect example without synchronized protection
if (queue.isEmpty()) {
    queue.wait(); // assume no exception is thrown
}

In this scenario, Thread A checks the queue, finds it empty, and is about to call wait(). At the same moment, Thread B inserts an element and calls notify(). Thread A then executes wait() after the notification, causing it to sleep forever because the notification was missed. This classic lost‑wake‑up problem stems from the time window between condition check and waiting.

The synchronized block eliminates this window. Thread A first acquires the lock, checks the condition, and if the condition is false, calls wait(). Because wait() releases the lock only after the thread has been placed in the WaitSet, Thread B cannot modify the condition until Thread A has released the lock, ensuring that the subsequent notify() will awaken a thread that is already waiting.

After a thread is notified, it does not resume execution immediately. It must re‑acquire the monitor before wait() can return. The HotSpot source shows this re‑entry step:

// Re‑entry after being awakened
if (v == ObjectWaiter::TS_RUN) {
    enter(current);
} else {
    ReenterI(current, &node);
}

This extra lock‑competition explains why many novices mistakenly think a notified thread runs straight away.

The canonical pattern for using wait() and notify() is therefore:

synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    // business logic
}

synchronized (lock) {
    // modify condition
    lock.notifyAll();
}

The while loop guards against spurious wake‑ups, as the documentation states that a thread may wake without a corresponding notify(). Using notifyAll() instead of notify() avoids waking the wrong thread when multiple waiters are present.

In summary, wait() must be called while holding the monitor because its implementation releases the lock as part of its operation. The JVM enforces ownership with a hard check, and the synchronized block guarantees atomicity of condition testing and waiting, preventing lost wake‑ups and ensuring correct thread coordination.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javasynchronizedmonitornotifywait
samdeepthink
Written by

samdeepthink

Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.