Why new Thread() Doesn't Create a Thread: Tracing Java Thread Birth from JVM to OS

This article traces the complete lifecycle of a Java thread from new Thread() through start() to sleep and interrupt, revealing how the JVM creates OS threads via C++ JavaThread and OSThread objects, and demonstrating the 1:1 thread model with executable verification code.

samdeepthink
samdeepthink
samdeepthink
Why new Thread() Doesn't Create a Thread: Tracing Java Thread Birth from JVM to OS
new Thread()

only creates a Java object — no real thread exists yet. The Thread constructor initializes fields like threadStatus = 0 (NEW), interrupted = false, and stores the target Runnable. No native calls, no C++ objects, no OS thread allocation occur. isAlive() returns false and getState() returns NEW.

Thread t = new Thread(() -> { }, "my-worker");
System.out.println("state=" + t.getState());
System.out.println("isAlive=" + t.isAlive());

Output:

state=NEW
isAlive=false

start() Creates the Real Thread

Calling start() checks threadStatus == 0 (throws IllegalThreadStateException if not), then invokes the native start0() → JVM_StartThread(). This JVM function does three things:

Creates a JavaThread C++ object — JVM's internal representation of a Java thread. Its constructor calls os::create_thread().

Creates the OS thread — On Windows, HotSpot calls _beginthreadex() with CREATE_SUSPENDED flag, so the thread starts suspended. An OSThread C++ object is also created to hold the OS thread handle and ID.

Links Java and JVM objects — prepare() cross-references the Java Thread object and the C++ JavaThread object.

Finally, Thread::start() sets the Java thread state to RUNNABLE and calls os::start_thread() → ResumeThread() to wake the OS thread. The OS thread then invokes the Java run() method.

Thread.start()
 → start0() (native)
   → JVM_StartThread()
     → new JavaThread()           // JVM C++ object
       → os::create_thread()      // Create OS thread (suspended)
         → _beginthreadex()       // Windows API
       → prepare()                // Link Java and JVM objects
       → Thread::start()
         → os::start_thread()     // Resume OS thread
           → ResumeThread()       // Windows API

Four-Layer Thread Structure

A running Java thread consists of four one-to-one mapped layers:

Java layer : java.lang.Thread — The Thread object you manipulate in code

JVM layer : JavaThread (C++) — JVM-internal thread management object

JVM layer : OSThread (C++) — Holds OS thread handle and ID

OS layer : OS Thread — OS-scheduled execution unit

Java-layer method calls traverse down through JavaThread → OSThread → OS thread. OS thread state changes propagate back up to Thread.getState().

What sleep() Does Across Layers

Thread.sleep()

is native → JVM_Sleep:

Checks interrupt flag; if set, throws InterruptedException immediately.

Sets OSThread state to SLEEPING; reflects as TIMED_WAITING in getState().

Calls ParkEvent.park(millis) — JVM's internal synchronization primitive. On Windows, this eventually calls WaitForSingleObject(), putting the OS thread in a wait state (no CPU time slices).

When time expires or interrupt occurs, park returns, OSThread state restores, thread becomes runnable again.

How interrupt() Wakes a Sleeping Thread

thread.interrupt()

sets the Java interrupted field to true, then calls native interrupt0() → JVM_Interrupt → JavaThread::interrupt(), which does four things:

void JavaThread::interrupt() {
  // Set OS-level interrupt flag
  osthread()->set_interrupted(true);
  // Wake thread sleeping via Thread.sleep()
  _SleepEvent->unpark();
  // Wake thread blocked in LockSupport.park()
  parker()->unpark();
  // Wake thread blocked in ObjectMonitor.wait()
  _ParkEvent->unpark();
}

The key is _SleepEvent->unpark(). The sleeping thread waits on a ParkEvent; unpark wakes it. The thread returns from park, checks is_interrupted(true) (which clears the flag), sees true, returns false to JVM_Sleep, which throws InterruptedException. That's why isInterrupted() returns false after catching the exception — the flag was cleared during the check.

Verification Code and Output

File ThreadLifecycle.java, run with java ThreadLifecycle.java:

public class ThreadLifecycle {
  public static void main(String[] args) throws Exception {
    // step 1: new Thread() - only a Java object, no real thread yet
    Thread t = new Thread(() -> {
      try {
        System.out.println("  [worker] started, state=" + Thread.currentThread().getState());
        System.out.println("  [worker] thread id=" + Thread.currentThread().getId());
        System.out.println("  [worker] sleeping 5 seconds...");
        Thread.sleep(5000);
        System.out.println("  [worker] woke up normally");
      } catch (InterruptedException e) {
        System.out.println("  [worker] interrupted during sleep!");
        System.out.println("  [worker] isInterrupted=" + Thread.currentThread().isInterrupted());
      }
    }, "my-worker");

    System.out.println("[main] after new Thread():");
    System.out.println("  state=" + t.getState());
    System.out.println("  isAlive=" + t.isAlive());

    // step 2: start() - now JVM creates the real thread
    System.out.println("[main] calling start()...");
    t.start();
    Thread.sleep(200);

    System.out.println("[main] after start():");
    System.out.println("  state=" + t.getState());
    System.out.println("  isAlive=" + t.isAlive());

    // step 3: wait a moment, worker is sleeping
    Thread.sleep(500);
    System.out.println("[main] worker is now sleeping, state=" + t.getState());

    // step 4: interrupt the sleeping worker
    System.out.println("[main] calling interrupt()...");
    t.interrupt();

    t.join();
    System.out.println("[main] worker finished, state=" + t.getState());
  }
}

Consistent output across runs:

run 1:
[main] after new Thread():
  state=NEW
  isAlive=false
[main] calling start()...
  [worker] started, state=RUNNABLE
  [worker] thread id=23
  [worker] sleeping 5 seconds...
[main] after start():
  state=TIMED_WAITING
  isAlive=true
[main] worker is now sleeping, state=TIMED_WAITING
[main] calling interrupt()...
  [worker] interrupted during sleep!
  [worker] isInterrupted=false
[main] worker finished, state=TERMINATED

Key observations from output:

After new Thread(): state NEW, isAlive=false — no real thread.

After start(): worker runs, state RUNNABLE; after sleep, state TIMED_WAITING — Java thread state maps to JVM/OS layers. interrupt() wakes worker immediately (no 5-second wait), catches InterruptedException, isInterrupted() returns false because flag cleared on check.

Final state: TERMINATED.

Summary

JDK 17 uses a 1:1 thread model: one Java Thread ↔ one JVM JavaThread ↔ one OS native thread. new Thread() only creates the mapping's starting point; the real thread exists only after start().

Note: JDK 21 introduces virtual threads with an M:N model (many virtual threads multiplexed onto few OS threads), fundamentally different from the 1:1 model described here.

Reference: Java 17 OpenJDK

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.

JavaJVMconcurrencyThreadInterruptSleepJDK 17Thread Lifecycle1:1 Thread ModelOS Thread
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.