← Back to list

Thread State and Priorities: How the JVM Schedules, Pauses, and Advances Your Code

Learn Java thread states (NEW→RUNNABLE→BLOCKED→WAITING→TERMINATED) and priorities — how the JVM decides which thread runs and when.

Codio.dev · 2026-03-31 12:31 · 0 claps · 6.3 min read paywalled
#java #spring-boot #system-design-interview #software-development #multithreading
Open on Medium ↗
Wiki topics: 📚 · Books & Reading

Thread State and Priorities: How the JVM Schedules, Pauses, and Advances Your Code

Learn Java thread states (NEW→RUNNABLE→BLOCKED→WAITING→TERMINATED) and priorities — how the JVM decides which thread runs and when.

Section 2 — Start Simple (Zero → Basic)

Imagine you’re at a busy airport. There are many passengers (threads), but only a few security gates (CPU cores) are open. Some passengers are in line waiting (RUNNABLE), some are sitting at the lounge waiting for a boarding call (WAITING), some are stuck because the gate agent needs their passport checked (BLOCKED), and one is already on the plane (TERMINATED). The airport can only process a few passengers at a time, and a VIP flyer (high-priority thread) might get moved ahead in line.

That, in a nutshell, is thread states and priorities.

Every thread in Java is always in exactly one of 6 states. The JVM and OS together decide who gets to “board” (use the CPU), for how long, and what happens when they have to step aside.

Section 3 — Build Intuition

Why does this exist? Your computer has 8–16 CPU cores. But a typical Java app has hundreds of threads — HTTP request handlers, background jobs, GC threads, RabbitMQ consumers, etc. There’s no way everyone runs at once. The JVM needs a traffic controller system.

The problem it solves: Without thread states and scheduling, threads would fight over the CPU chaotically, starving each other. With it, the JVM can:

  • Park a thread when it’s waiting for a DB response (instead of wasting CPU spinning)
  • Wake it up the moment data arrives
  • Give critical threads (like payment processing) more CPU time than background tasks

Section 4 — Step-by-Step Working

Let’s walk through the 6 thread states and their transitions one step at a time.

Here’s what each transition means in plain language:

Step 1 — NEW: You call new Thread(...). The thread object exists in memory but hasn't started. No CPU resources consumed yet.

Step 2 — RUNNABLE: You call .start(). The thread is now either actively running on a CPU core, or sitting in the OS scheduler's ready queue. Java combines both into one state — you can't tell from the JVM side which it is.

Step 3 — BLOCKED: The thread tries to enter a synchronized block but another thread holds the lock. It parks here, consuming zero CPU, until the lock is free.

Step 4 — WAITING: The thread called object.wait(), Thread.join(), or LockSupport.park() with no timeout. It'll stay here forever until someone explicitly wakes it via notify() or unpark().

Step 5 — TIMED_WAITING: Same as WAITING but with a deadline — Thread.sleep(1000), wait(timeout), parkNanos(...). Automatically wakes up after time expires.

Step 6 — TERMINATED: The run() method returned (or threw an uncaught exception). The thread is dead — you cannot restart it.

Section 5 — Visual Learning: Thread Priority

Now let’s look at priorities — how the JVM decides who goes first.

Section 6 — Code + Explanation

Simple example: Observing thread states

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {

        // 1. Thread is created — state: NEW
        Thread worker = new Thread(() -> {
            try {
                // 3. Inside run() — state: RUNNABLE
                Thread.sleep(2000);     // state becomes: TIMED_WAITING
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        System.out.println(worker.getState()); // → NEW

        worker.start();                         // → RUNNABLE
        System.out.println(worker.getState()); // → RUNNABLE

        Thread.sleep(100);                      // give worker time to sleep
        System.out.println(worker.getState()); // → TIMED_WAITING

        worker.join();                          // wait for it to finish
        System.out.println(worker.getState()); // → TERMINATED
    }
}

Line by line:

  • new Thread(...) — allocates the thread object, state = NEW, nothing running yet
  • worker.start() — tells the JVM to schedule it; state flips to RUNNABLE
  • Thread.sleep(100) — after the worker calls sleep(2000) internally, its state is TIMED_WAITING
  • worker.join() — the main thread blocks until worker finishes
  • After join — the worker is TERMINATED

Advanced example: Observing BLOCKED vs WAITING

java

public class BlockedVsWaiting {
private static final Object LOCK = new Object();
    public static void main(String[] args) throws InterruptedException {
        // Thread A holds the lock and waits on it
        Thread threadA = new Thread(() -> {
            synchronized (LOCK) {
                try {
                    LOCK.wait();        // releases lock, enters WAITING
                } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            }
        }, "Thread-A");
        // Thread B tries to grab the same lock → BLOCKED (can't enter sync block)
        Thread threadB = new Thread(() -> {
            synchronized (LOCK) {      // BLOCKED here until A releases (which wait() does)
                System.out.println("Thread-B got the lock");
            }
        }, "Thread-B");
        threadA.start();
        Thread.sleep(100);   // let A acquire lock and call wait()
        threadB.start();
        Thread.sleep(100);   // let B try to acquire lock
        System.out.println("A = " + threadA.getState()); // → WAITING
        System.out.println("B = " + threadB.getState()); // → BLOCKED
        synchronized (LOCK) { LOCK.notify(); } // wake up A
    }
}

What changed and why: Here we see the critical difference — wait() releases the lock and parks the thread (WAITING), while Thread B can't even enter the synchronized block and is stuck BLOCKED.

Section 7 — Connect to Real-World Systems

In Spring Boot @Async methods: When you use thenComposeAsync(fn, executor), the CompletableFuture framework parks the continuation in WAITING until the dependent stage completes, then re-submits it to your executor pool as RUNNABLE. The state transitions happen entirely inside the framework.

In Kubernetes HPA and thread dumps: When you see many threads in BLOCKED state in a thread dump — that’s contention on a shared lock. When you see many in TIMED_WAITING — likely too many sleep() calls or timeouts in your code. This is exactly the signal you'd look for when diagnosing the connection issues under load.

Section 8 — Go Deeper (Intermediate → Advanced)

Advanced: Priority internals + edge cases

OS-level reality: Java thread priorities map to OS-level priorities. On Linux (which your pods run on), the JVM uses pthread with SCHED_OTHER policy by default. The OS treats priorities as "niceness hints," not hard guarantees. Two threads with priority 10 and 5 might get equal CPU time if the OS decides so. This is why you should never depend on priorities for correctness — only use them as performance hints.

Thread starvation: If you have many high-priority threads, low-priority threads may never run — this is called starvation. In practice, modern OSes prevent it with aging (gradually boosting priority of waiting threads), but it’s a real risk with custom schedulers.

Priority inheritance in synchronized blocks: Java does not implement priority inheritance for monitors. If a high-priority thread is blocked waiting for a low-priority thread holding a lock, the low-priority thread does not get boosted. This can cause priority inversion — a known hazard in real-time systems.

Section 9 — Common Mistakes

Mistake 1: “RUNNABLE means the thread is running” Wrong. RUNNABLE means it’s either running OR waiting for a CPU turn. A RUNNABLE thread may sit in the OS run queue for milliseconds before getting CPU time. Don’t assume RUNNABLE = active work.

Mistake 2: “Setting priority 10 makes my thread faster” Not necessarily. The OS is the final arbiter. On Linux with SCHED_OTHER, all Java threads compete under the same policy. MAX_PRIORITY only means it might get scheduled slightly more aggressively — not that it runs exclusively.

Mistake 3: “I can restart a TERMINATED thread” False. Once terminated, the thread object is dead. Calling .start() again throws IllegalThreadStateException. You need new Thread(...).

Mistake 4: “BLOCKED and WAITING are the same thing” They feel similar (both = parked) but the cause and wake mechanism are totally different. BLOCKED = lock competition (wakes automatically when lock is free). WAITING = explicit coordination (wakes only on notify() or unpark()). This distinction is critical when reading thread dumps.

Mistake 5: “Thread.sleep() holds locks while sleeping” False. sleep() does NOT release any monitor locks the thread holds. If a thread holds a synchronized lock and calls sleep(), other threads trying to acquire that lock remain BLOCKED for the entire sleep duration. Use wait() if you want to release the lock while pausing.

Section 10 — Summary

  • Java threads have 6 states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
  • NEW = object exists, not scheduled. RUNNABLE = running or in OS queue. BLOCKED = waiting for a monitor lock. WAITING = parked indefinitely by wait()/park(). TIMED_WAITING = parked with a deadline. TERMINATED = dead, non-restartable
  • BLOCKED vs WAITING is the most important distinction: BLOCKED is about lock competition, WAITING is about explicit coordination
  • sleep() keeps locks; wait() releases them
  • Thread priorities (1–10) are hints, not guarantees. Default is NORM_PRIORITY (5)
  • Priority inversion (low-priority thread holding a lock a high-priority thread needs) is a real hazard
  • A thread dump showing many BLOCKED threads = lock contention. Many WAITING threads = normal for well-designed consumer pools

One-line intuition: A thread is like a person in multiple possible waiting situations — in a queue for the cashier (RUNNABLE), locked out of a room (BLOCKED), waiting for a phone call (WAITING), or taking a timed nap (TIMED_WAITING).

Section 11 — Conclusion

You’ve gone from “what even is a thread state?” all the way to priority inversion, reading thread dumps, and writing monitoring code for production services. That’s a solid arc.

The concept of thread states is one of those things that separates engineers who “write concurrent code” from engineers who truly understand it

THANKS FOR READING


메타데이터
post_id
b39272f8fea5
slug
thread-state-and-priorities-how-the-jvm-schedules-pauses-and-advances-your-code-b39272f8fea5
url
https://medium.com/@Codio.dev/thread-state-and-priorities-how-the-jvm-schedules-pauses-and-advances-your-code-b39272f8fea5
canonical_url
https://medium.com/@Codio.dev/thread-state-and-priorities-how-the-jvm-schedules-pauses-and-advances-your-code-b39272f8fea5
author_url
https://medium.com/@Codio.dev
status
ok
fetched_at
2026-07-13 06:23:13