Skip to content
Java multithreading 7 min read

Thread.sleep()

Thread.sleep() tells the current thread to pause execution for a specified amount of time, handing control back to the CPU for other threads to run. It is one of the most frequently used methods in concurrent Java — and one of the most frequently misused. This page covers the API, the common traps, and what is actually happening at the OS and JVM level when you call it.

The sleep() API

Thread.sleep() is a static native method in java.lang.Thread. It always applies to the currently executing thread — you cannot sleep another thread from the outside.

// Overload 1: milliseconds only
Thread.sleep(long millis) throws InterruptedException

// Overload 2: milliseconds + nanoseconds (finer granularity)
Thread.sleep(long millis, int nanos) throws InterruptedException

The method throws InterruptedException — a checked exception — so you must either declare it with throws or catch it. More on that in a moment.

Note: Since Java 19, Thread.sleep(Duration duration) is also available, accepting a java.time.Duration for more readable code.


Basic Usage

public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Start: " + System.currentTimeMillis());

        Thread.sleep(2000); // pause for 2 seconds

        System.out.println("End:   " + System.currentTimeMillis());
    }
}

Output:

Start: 1718280000000
End:   1718280002003

The gap is approximately 2000 ms. The small overshoot (3 ms here) is normal — the OS scheduler does not guarantee precision to the millisecond.

Warning: Thread.sleep() does not guarantee an exact pause duration. The thread will sleep at least the requested time, but may wake up slightly later depending on system load and OS scheduling resolution. Never use it for high-precision timing.


Sleeping a Specific Thread

Because sleep() is static, it always affects the calling thread, not the Thread object you might have a reference to:

Thread worker = new Thread(() -> {
    try {
        System.out.println("Worker sleeping…");
        Thread.sleep(1000); // sleeps THIS thread (worker), not main
        System.out.println("Worker awake!");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // restore interrupt flag
        System.out.println("Worker was interrupted!");
    }
});

worker.start();
worker.join(); // main waits for worker to finish

Output:

Worker sleeping…
Worker awake!

Handling InterruptedException Correctly

This is the most critical part of using sleep(). When another thread calls interrupt() on a sleeping thread, sleep() throws InterruptedException and clears the thread’s interrupt flag.

The wrong way — silently swallowing the exception

// BAD: interrupt signal is lost forever
try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    // do nothing — the interrupt is gone
}

The right way — restore the interrupt flag

// GOOD: restore the flag so callers can detect it
try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // re-set the flag
    // optionally: log, clean up, or return early
}

Tip: If your method signature declares throws InterruptedException, you can simply let the exception propagate instead of catching it yourself. This is often the cleanest approach in library or framework code.

Propagating vs catching — when to choose which

ScenarioRecommended approach
Short utility method used in concurrent contextthrows InterruptedException — propagate it
Runnable.run() (cannot declare checked exceptions)Catch and call Thread.currentThread().interrupt()
Top-level task that should abort on interruptCatch, clean up resources, then return or log

Real-World Pattern: Polling with Sleep

A common pattern is polling a condition at intervals. Use a loop that checks whether the thread has been interrupted so it can shut down cleanly:

public class PollingTask implements Runnable {
    private volatile boolean dataReady = false; // set by another thread

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            if (dataReady) {
                System.out.println("Data is ready — processing!");
                break;
            }
            System.out.println("Waiting for data…");
            try {
                Thread.sleep(500); // check every 500 ms
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // restore flag
                break; // exit the loop on interrupt
            }
        }
        System.out.println("Task finished.");
    }
}

Note: For production polling scenarios with precise intervals, consider ScheduledExecutorService from java.util.concurrent instead — it is more accurate and handles thread management for you.


Using TimeUnit for Readable Sleep Calls

The java.util.concurrent.TimeUnit enum wraps Thread.sleep() with human-readable time units:

import java.util.concurrent.TimeUnit;

public class TimeUnitSleepDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Sleeping for 3 seconds…");
        TimeUnit.SECONDS.sleep(3);         // clearer than Thread.sleep(3000)

        System.out.println("Sleeping for 500 milliseconds…");
        TimeUnit.MILLISECONDS.sleep(500);

        System.out.println("Done.");
    }
}

TimeUnit.sleep() delegates to Thread.sleep() internally, so the behaviour and InterruptedException handling rules are exactly the same.

Tip: Prefer TimeUnit.SECONDS.sleep(n) over Thread.sleep(n * 1000) in any code that will be read by others — the intent is immediately clear.


Java 19+ Duration-Based sleep

Java 19 introduced a convenience overload that accepts a java.time.Duration:

import java.time.Duration;

// Java 19+
Thread.sleep(Duration.ofSeconds(2));
Thread.sleep(Duration.ofMillis(750));

This integrates naturally with the Date/Time API and eliminates unit-conversion arithmetic.


Does sleep() Release Locks?

No — and this is a critical distinction:

  • Thread.sleep() does not release any synchronized monitors the sleeping thread holds.
  • Object.wait() does release the monitor, allowing other threads to enter the synchronized block.
public class SleepDoesNotReleaseLock {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread holder = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Holder: acquired lock, sleeping…");
                try {
                    Thread.sleep(3000); // holds lock while sleeping!
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Holder: woke up, releasing lock.");
            }
        });

        Thread waiter = new Thread(() -> {
            System.out.println("Waiter: trying to acquire lock…");
            synchronized (lock) {              // blocks until holder wakes
                System.out.println("Waiter: finally got the lock.");
            }
        });

        holder.start();
        Thread.sleep(100); // let holder acquire lock first
        waiter.start();
        holder.join();
        waiter.join();
    }
}

Output:

Holder: acquired lock, sleeping…
Waiter: trying to acquire lock…
Holder: woke up, releasing lock.
Waiter: finally got the lock.

The waiter is blocked for the full 3 seconds because sleep() keeps the lock. If you need to release a lock while waiting, use wait() / notify() from Inter-Thread Communication.


Under the Hood

What happens in the JVM and OS

When your thread calls Thread.sleep(millis):

  1. The JVM calls the native sleep system call (e.g., nanosleep on Linux, SleepEx on Windows).
  2. The OS moves the thread from the RUNNABLE state to a timed-waiting queue (no CPU cycles consumed).
  3. A hardware timer is set. When it fires, the OS moves the thread back to the runnable queue.
  4. The JVM’s thread scheduler eventually picks the thread up and execution resumes after the sleep() call.

The JVM reflects this as Thread.State.TIMED_WAITING. You can observe it:

Thread t = new Thread(() -> {
    try { Thread.sleep(5000); }
    catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
t.start();
Thread.sleep(100); // let t start sleeping
System.out.println(t.getState()); // TIMED_WAITING

Output:

TIMED_WAITING

Why precision is not guaranteed

Modern operating systems use a timer interrupt frequency (the “tick rate”) — commonly 100–1000 Hz on Linux, meaning the minimum timer resolution is 1–10 ms. A sleep(1) call may actually sleep for 1–15 ms depending on the platform. On Windows this is particularly noticeable until timeBeginPeriod(1) is called (which the JVM does in some configurations).

Sleep vs busy-wait

Never replace Thread.sleep() with a busy-wait loop to “check faster”:

// BAD: burns CPU on the calling thread
long end = System.currentTimeMillis() + 2000;
while (System.currentTimeMillis() < end) { /* spin */ }

// GOOD: zero CPU while waiting
Thread.sleep(2000);

A sleeping thread consumes essentially zero CPU. A spinning thread consumes 100% of one core.

Virtual threads and sleep (Java 21)

With Virtual Threads (Project Loom, Java 21), calling Thread.sleep() on a virtual thread does not block the underlying OS carrier thread. The virtual thread is unmounted from the carrier, freeing it to run other virtual threads. This makes sleep()-heavy code scale to millions of virtual threads without the OS overhead you would incur with platform threads.


Common Mistakes Summary

MistakeWhy it hurtsFix
Swallowing InterruptedException with empty catchInterrupt signal is lost; thread cannot be shut down cleanlyCall Thread.currentThread().interrupt() in the catch block
Calling someThread.sleep(...) expecting it to sleep someThreadIt sleeps the calling thread, not someThreadUse Thread.sleep() inside the target thread’s run()
Using sleep() inside a synchronized block expecting lock releaseLock is held the whole timeUse wait() / notify() for lock-releasing waits
Assuming millisecond precisionOS scheduling can add extra delayAccept the jitter; use ScheduledExecutorService for accuracy

  • Thread Life Cycle — See how TIMED_WAITING fits into the full lifecycle from NEW to TERMINATED.
  • Inter-Thread Communication — Use wait() and notify() when you need to release a lock while a thread waits.
  • Interrupting a Thread — Understand how interrupt(), isInterrupted(), and InterruptedException work together.
  • Creating a Thread — The foundation: how to create and start threads before you pause them.
  • Synchronization — Learn how synchronized monitors interact with sleeping threads.
  • Thread Pools & Executors — Production-grade scheduling with ScheduledExecutorService instead of manual sleep loops.
Last updated June 13, 2026
Was this helpful?