> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Sleep vs Wait in Java: Which One Releases the Lock?
- URL: https://www.ggorantala.dev/java-sleep-vs-wait/
- Published: 2026-10-30T13:49:44.000Z
- Updated: 2026-10-30T13:49:44.000Z
- Author: Gopi Gorantala
- Tags: java-threads, java-concurrency, java-interview, thread-safety

On JDK 21, with the virtual-thread scheduler pinned to one carrier thread, four virtual threads that each do `Thread.sleep(300)` finish in 331 ms. Put the same sleep inside a `synchronized` block and the total is 1,219 ms. Swap the sleep for `wait(300)` inside the block and you get 1,227 ms.

Same 300 ms pause, three different answers. That's the sleep vs wait in Java question wearing a disguise, and it's why I stopped trusting the one-line answer ("sleep keeps the lock, wait releases it"). The line is true. It's also about a fifth of what you need.

## Which one lets go of the lock?

`Thread.sleep()` pauses the current thread and keeps every monitor it holds. `Object.wait()` pauses the current thread, releases the monitor of the one object you called it on, and puts you in that object's wait set until someone notifies you, interrupts you, or the timeout runs out. That's the whole difference in mechanism. Everything else below falls out of it.

Which means the two calls aren't really rivals. `sleep` answers "I want to do nothing for a while." `wait` answers "I can't continue until some condition becomes true, and I'll step aside so whoever can make it true gets in." People mix them up because both show up as a thread that isn't running, and both throw `InterruptedException`.

I wrote this one after a review where someone replaced a polling `sleep(50)` loop with `wait(50)` "to be safe." It worked, so nobody questioned it. It also did nothing useful. Took me longer than I'd like to admit to explain why. The explanation, with numbers, is below; I ran everything on OpenJDK 21.0.12.1, 2 vCPU x86\_64\. JDK 25 is the latest GA according to openjdk.org/projects/jdk, and I did not run on it.

## The table I wish I'd had

|                                 | Thread.sleep                                  | Object.wait                                                                        |
| ------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------- |
| Monitors held while paused      | Keeps all of them                             | Releases the one it was called on; keeps any others                                |
| Needs to hold a monitor to call | No                                            | Yes, on that object, or IllegalMonitorStateException                               |
| Who can end the pause early     | Only an interrupt                             | notify, notifyAll, interrupt, timeout, or a spurious wakeup                        |
| Zero argument                   | sleep(0) returns almost at once               | wait(0) waits with no timeout, forever                                             |
| State in a thread dump          | TIMED\_WAITING (sleeping)                     | TIMED\_WAITING (on object monitor) or WAITING (on object monitor)                  |
| Return value                    | Returns normally after the time               | void: no hint whether it timed out or was notified                                 |
| After an interrupt              | Flag cleared, InterruptedException            | Flag cleared, monitor re-acquired first, then the exception                        |
| Virtual thread on JDK 21        | Unmounts, unless inside synchronized          | Pins the carrier inside synchronized, but the scheduler adds a carrier (see below) |
| Where it lives                  | Thread, static (sleep(Duration) since 19)     | Object, instance, on every object                                                  |
| Typical honest use              | Backoff, pacing, tests that need time to pass | Guarded waiting inside a while loop (better: a Condition or a BlockingQueue)       |

The Javadoc for `wait` says its timeout holds "more or less." `sleep` says it's "subject to the precision and accuracy of system timers and schedulers." Neither gives you a hard real-time number.

## Six differences, each with the code that shows it

**1\. Who else can get in.** This is the one that causes outages.

```java
synchronized (cache) {
    cache.wait(2000);      // other threads can enter synchronized(cache) right now
}
synchronized (cache) {
    Thread.sleep(2000);    // every thread that wants this monitor queues behind you
}

```

```log
Timeline (monitor = cache)

sleep:  T1 |#### locked, paused 2000 ms ####|
        T2    . . . BLOCKED . . . . . . . . . |enter|

wait:   T1 |lock|--- waiting, monitor free ---|re-lock|
        T2       |enter, work, exit|

```

**2\. Who can wake you.** I sent `notifyAll()` at about 100 ms to a thread in `sleep(1500)` and to one in `wait(1500)`. The sleeper slept 1,500 ms. The waiter woke after 100 ms. `sleep` has no wake-up channel except interruption, because nothing is waiting *for* anything.

**3\. `wait` needs the monitor, `sleep` doesn't.**

```log
wait outside synchronized -> java.lang.IllegalMonitorStateException: current thread is not owner
sleep outside synchronized -> fine

```

**4\. Zero means opposite things.** `Thread.sleep(0)` returned in 1 ms. A thread in `wait(0)` was still `WAITING` 800 ms later and ended only because I interrupted it. If you compute a remaining timeout and it lands on 0, `wait(remaining)` can hang a thread forever. Guard it, or use `System.nanoTime()` deadlines like the example in the `Object.wait` Javadoc.

**5\. `wait` tells you nothing about why it returned.** I ran `wait(500)` twice, once with nobody notifying and once with a `notifyAll` at 100 ms. Both returned normally; one after 500 ms, one after 100 ms. The method is `void`. The condition in your `while` loop is the only way to find out, which is the whole point of that loop. The rules for writing it properly live in [wait(), notify() and notifyAll() in Java](https://ggorantala.dev/java-wait-notify-notifyall/?ref=ggorantala.dev), so I'll leave it there.

**6\. Interrupt behaves almost the same, with one extra step.** Both calls wake at once, both clear the interrupt flag, both throw. With `wait`, the thread first re-acquires the monitor, so inside the `catch` block `Thread.holdsLock(lock)` printed `true`. Same as `sleep`, but for a different reason: `sleep` never let go, `wait` took it back. If another thread is holding that monitor when you get interrupted, you sit `BLOCKED` before the exception arrives. What to do in that `catch` block is covered in [the interruption article](https://ggorantala.dev/java-thread-interrupt-interruptedexception/?ref=ggorantala.dev).

## One lab, both calls, and the dumps

Here's the one I used for items 1 and 2\. Pinned: OpenJDK 21.0.12.1 (Ubuntu build), 2 vCPU, x86\_64\. Save as `Lab.java`:

```java
import java.util.concurrent.CountDownLatch;

public class Lab {
    static final Object LOCK = new Object();

    public static void main(String[] args) throws Exception {
        String how = args[0];                       // "sleep" or "wait"
        CountDownLatch inside = new CountDownLatch(1);
        Thread holder = new Thread(() -> {
            synchronized (LOCK) {
                inside.countDown();
                try {
                    if (how.equals("sleep")) Thread.sleep(2000); // keeps LOCK
                    else LOCK.wait(2000);                        // releases LOCK
                } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }
        }, "holder");
        holder.start();
        inside.await();
        Thread.sleep(100);                          // make sure holder is parked

        long t0 = System.nanoTime();
        Thread second = new Thread(() -> {
            synchronized (LOCK) {
                System.out.println("second got monitor after "
                    + (System.nanoTime() - t0) / 1_000_000 + " ms");
            }
        }, "second");
        second.start();
        second.join();
        holder.join();
    }
}

```

```bash
java Lab.java sleep
java Lab.java wait

```

```log
second got monitor after 1900 ms     # sleep: it waited out the holder's remaining 1,900 ms
second got monitor after 3 ms        # wait: in at once

```

Both numbers repeated on every run I made. This isn't a race, so it's the one lab in this series that doesn't need a start gate or 10,000 iterations. The 100 ms head start is only there so the holder has actually reached its pause.

To see the dump, make the pause 10 seconds, wait a second, and run `jcmd <pid> Thread.print`. With `sleep`:

```log
"holder" ... waiting on condition
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep0(Native Method)
	at java.lang.Thread.sleep(Thread.java:509)
	- locked <0x00000000a851d398> (a java.lang.Object)

"second" ... waiting for monitor entry
   java.lang.Thread.State: BLOCKED (on object monitor)
	- waiting to lock <0x00000000a851d398> (a java.lang.Object)

```

With `wait`, the holder looks like this and `second` is gone, because it already finished:

```log
"holder" ... in Object.wait()
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
	at java.lang.Object.wait0(Native Method)
	- waiting on <0x00000000a851d398> (a java.lang.Object)
	at java.lang.Object.wait(Object.java:366)
	- locked <0x00000000a851d398> (a java.lang.Object)

```

Look at that last line. The dump says `waiting on` and `locked` with the same address, for a monitor the thread has just released. My guess is the `locked` line comes from the frame's lock record rather than from current ownership; I haven't confirmed that in the HotSpot source. If you're hunting a lock owner in a dump, `BLOCKED` threads with `waiting to lock <addr>` are the evidence. A `locked <addr>` line sitting under `Object.wait` is not. I read it wrong the first time.

My first version of this lab had `second` call `notifyAll()` when it got in. That woke the holder, so the holder had already finished by the time I looked and my dump showed neither thread. I spent a while convinced `jcmd` was broken. It wasn't. (The second time it was a `grep` buffering the PID line I was waiting for. Pipes and `grep` buffer; files don't.)

If your `second` thread shows up `BLOCKED` and you expected a `wait`, check who's *inside* the monitor with `ThreadMXBean.getThreadInfo(id).getLockOwnerName()`. In the `sleep` run that returned `holder`. That's contention, with a named culprit, not a deadlock.

## Where the wrong pick shows up in production

**Sleeping while holding a lock.** A cache loader retries a failed fetch with `Thread.sleep(500)` inside `synchronized (cache)`. When the backend is slow, every reader of that cache queues behind the sleeper. Dashboards show request latency rising by multiples of 500 ms, CPU near idle, and a pile of `BLOCKED` threads all pointing at one address. CPU at 2% and every thread paused is what a sleep-under-lock looks like.

**Polling with sleep where you wanted a signal.** I measured 40 rounds each of "consumer notices a flag flip", on the 2-vCPU box. Polling with `sleep(50)`: mean 23,511 µs, worst 47,401 µs. `wait`/`notify`: mean 184 µs, worst 778 µs. Roughly 130 times slower on average, and the polling version wakes up 20 times a second to learn nothing. For the replacement I mentioned at the top: `wait(50)` in a loop with no `notify` anywhere is the same polling loop with a different name. It behaves identically, burns the same wakeups, and now throws `IllegalMonitorStateException` if somebody moves it outside the block.

**`sleep` as a test fix.** Adding `Thread.sleep(100)` until a flaky test goes green makes it pass on your laptop and fail on a 2-core CI runner. The race is still there. Use a latch or a `Future` from `java.util.concurrent` instead.

**Virtual threads and the 1,227 ms.** Back to the opening. With the scheduler pinned (`-Djdk.virtualThreadScheduler.parallelism=1 -Djdk.virtualThreadScheduler.maxPoolSize=1`):

```log
sleep-outside:  4 virtual threads x 300 ms -> total 331 ms
sleep-inside:   4 virtual threads x 300 ms -> total 1219 ms
wait-inside:    4 virtual threads x 300 ms -> total 1227 ms

```

Inside `synchronized`, both calls pin the single carrier on JDK 21, so the four pauses run one after another. Now the default scheduler, 2 carriers on this machine, each thread with its own lock:

```log
sleep-inside:  total 621 ms
wait-inside:   total 320 ms

```

Same pinning, different outcome. `wait` finished in one pause's time, so something added carriers; `sleep` ran two at a time. My reading is that `Object.wait` tells the scheduler it's about to block so it can compensate with another carrier, and `sleep` inside a monitor doesn't. That's inferred from these numbers, not from reading the source, so check it before you quote it. JEP 491 describes `Object.wait()` now unmounting a virtual thread, and takes pinning on `synchronized` away for both. The JEP page doesn't name a release; I'm taking JDK 24 from the release notes and haven't run 24 or 25\. The longer story is in [the virtual-thread pinning post](https://www.ggorantala.dev/java-virtual-thread-pinning-synchronized-jep-491/).

## A decision you can make in ten seconds

| You want to...                                           | Use                                                                                                          |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Wait for a condition another thread will make true       | wait in a while loop. Or a Condition, BlockingQueue, CountDownLatch; they exist so you don't hand-write this |
| Back off before a retry, holding no locks                | sleep (or sleep(Duration) since JDK 19)                                                                      |
| Pause for a fixed time and *also* want to be woken early | wait(timeout) in a loop, or Future.get(timeout)                                                              |
| Run something later or repeatedly                        | A ScheduledExecutorService, not a loop with sleep                                                            |
| Make a test pass                                         | Neither. Use a latch                                                                                         |
| Pause while holding a lock                               | Don't. Release first, then sleep                                                                             |

One question settles most of these: *is anyone going to tell me when to stop?* If yes, you want `wait` or one of its wrappers. If the answer is "the clock," you want `sleep`, and you should be holding nothing.

## The answer I'd give in an interview, and what comes next

"`sleep` pauses the thread and keeps its monitors, so anyone needing that lock stays `BLOCKED`. `wait` must be called while holding the object's monitor, releases that monitor while it waits, and is woken by `notify`, `notifyAll`, interrupt or timeout. In a dump, `sleep` is `TIMED_WAITING (sleeping)` and `wait` is `(on object monitor)`. Both clear the interrupt flag when they throw. And `wait` always sits in a `while` loop."

Expect this next: *"What does `wait(0)` do?"* (Forever. Not zero.) Or: *"The waiter was notified. Is it running?"* Not yet; it goes through `BLOCKED` until the notifier leaves its `synchronized` block. That one is in the [thread states article](https://ggorantala.dev/java-thread-lifecycle-states/?ref=ggorantala.dev) with a dump.

The first lab gave me the answer in ten minutes. The dump reading, the virtual-thread numbers and the `wait(0)` edge ate the rest of a long day, which is why it's all written down here.

## Questions that keep coming up

**Does `Thread.sleep()` release a lock in Java?** No. The Javadoc says the thread "does not lose ownership of any monitors." Anything else waiting for those monitors stays `BLOCKED` for the whole sleep.

**Does `wait()` release all my locks?** Only the monitor of the object you called it on. Any other `synchronized` object you hold stays locked while you wait, which is a classic way to build a deadlock.

**Can I call `wait()` without `synchronized`?** No. You'll get `IllegalMonitorStateException`. You must hold the object's monitor, from a `synchronized` block or method on that same object.

**Which is better in virtual threads?** On JDK 21, avoid both inside `synchronized`. On JDK 24 and later, JEP 491 changes this, but I measured only 21\. Sleep outside any lock unmounts cleanly; I saw that on 21.