Skip to content
Java multithreading 7 min read

run() vs start()

Two methods. Three letters each. One of the most common threading mistakes in Java: calling run() when you meant to call start(). The code compiles, it runs, it even prints the right output — but no new thread was ever created. This page explains exactly what each method does, why the difference matters, and what the JVM is doing behind the scenes.

The Short Answer

MethodWhat it doesNew thread created?
start()Asks the JVM to spawn a new OS thread, then calls run() on itYes
run()Executes the method body on the current thread like any normal method callNo

Warning: Calling run() directly is not an error — it just silently executes your task on the thread that made the call (usually main). No exception is thrown. This makes the bug easy to miss in small programs but devastating in production.


Seeing the Difference in Code

The clearest way to understand the distinction is to observe which thread runs the code:

public class RunVsStartDemo {
    public static void main(String[] args) {

        Thread t = new Thread(() ->
            System.out.println("Executing on: " + Thread.currentThread().getName())
        );

        System.out.println("--- Calling run() ---");
        t.run();   // NOT a new thread — runs on main

        System.out.println("--- Calling start() ---");
        t.start(); // Spawns a new thread
    }
}

Output:

--- Calling run() ---
Executing on: main
--- Calling start() ---
Executing on: Thread-0

The first call (run()) executes the lambda on the main thread — no concurrency at all. The second call (start()) hands the task to a freshly spawned thread named Thread-0.


A Deeper Look with Multiple Threads

The difference becomes even more obvious when you have two threads that are supposed to run concurrently:

public class ConcurrencyDemo {

    static void task(String name) {
        for (int i = 1; i <= 3; i++) {
            System.out.println(name + " - step " + i
                + " [" + Thread.currentThread().getName() + "]");
        }
    }

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

        Thread t1 = new Thread(() -> task("Alpha"));
        Thread t2 = new Thread(() -> task("Beta"));

        // Wrong: sequential execution, all on main thread
        System.out.println("=== Using run() ===");
        t1.run();
        t2.run();

        System.out.println();

        // Correct: concurrent execution, each on its own thread
        Thread t3 = new Thread(() -> task("Alpha"));
        Thread t4 = new Thread(() -> task("Beta"));
        System.out.println("=== Using start() ===");
        t3.start();
        t4.start();
        t3.join();
        t4.join();
    }
}

Output:

=== Using run() ===
Alpha - step 1 [main]
Alpha - step 2 [main]
Alpha - step 3 [main]
Beta - step 1 [main]
Beta - step 2 [main]
Beta - step 3 [main]

=== Using start() ===
Alpha - step 1 [Thread-2]
Beta - step 1 [Thread-3]
Alpha - step 2 [Thread-2]
Beta - step 2 [Thread-3]
...

With run() the output is perfectly sequential — no interleaving, no concurrency. With start() the two threads truly run in parallel (output order varies between runs).

Tip: Always check Thread.currentThread().getName() in your debug output when multithreading behaves unexpectedly. It immediately reveals whether you accidentally called run() instead of start().


When Is It Valid to Call run() Directly?

Rarely, but it does happen:

  • Unit testing a Runnable or Thread subclass’s logic in isolation, without needing actual concurrency.
  • Synchronous execution where you want the task to finish before the next line, and thread safety is handled elsewhere.
// Testing the Runnable logic synchronously — intentional
Runnable task = () -> System.out.println("Task executed");
task.run(); // Fine here — we're testing the logic, not the threading

In all other cases, use start() to get a real concurrent thread.


What Happens to Thread State

A Thread object moves through well-defined states (see Thread Life Cycle). Calling run() vs start() affects those states differently:

Thread t = new Thread(() -> {});

System.out.println(t.getState()); // NEW

// With start():
t.start();
System.out.println(t.getState()); // RUNNABLE (or TERMINATED if it finished fast)

// With run() — state never changes from NEW:
Thread t2 = new Thread(() -> {});
t2.run();
System.out.println(t2.getState()); // NEW  ← still NEW after run()!

Output:

NEW
RUNNABLE
NEW

Because run() is just a method call, the Thread object’s internal state machine is never touched. The thread stays NEW forever.


Calling start() Twice

Once a thread has been started, calling start() a second time — even after it has finished — throws an IllegalThreadStateException. See Starting a Thread Twice for the full explanation.

Thread t = new Thread(() -> System.out.println("running"));
t.start();
t.join();       // wait for it to finish
t.start();      // throws IllegalThreadStateException — thread is TERMINATED

Note: Calling run() multiple times does not throw — it is just a regular method. But again, each call runs on the current thread, not a new one.


Under the Hood

What start() Actually Does

When you call t.start(), the JVM executes a native method (start0) that:

  1. Validates the thread is in NEW state — throws IllegalThreadStateException otherwise.
  2. Allocates a native OS thread and a per-thread call stack (default size varies; tunable with -Xss).
  3. Transitions the Thread object’s state to RUNNABLE.
  4. Registers the thread with the JVM’s internal thread table.
  5. Hands the new thread to the OS scheduler, which eventually invokes Thread.run() on it.

The key point: start() involves a system call into the kernel. That is why thread creation has overhead — it is not a simple heap allocation. For high-throughput code, use a Thread Pool to reuse threads instead of creating new ones.

How Thread.run() Decides What to Execute

Thread itself implements Runnable. Its run() method checks whether a Runnable target was passed to the constructor:

// Simplified from java.lang.Thread source
private Runnable target;

@Override
public void run() {
    if (target != null) {
        target.run();   // delegates to the supplied Runnable
    }
    // If no target, the method body is empty — subclass overrides it instead
}
  • Runnable passed to constructorThread.run() delegates to target.run().
  • Thread subclass with overridden run() — the override replaces the method entirely; target is ignored.

This is why calling t.run() manually still works as a plain method — it just doesn’t go through the OS-thread machinery that start() triggers.

Stack Allocation per Thread

Each thread gets its own JVM stack. Local variables, method frames, and return addresses live here — completely isolated from other threads. The heap, however, is shared. This isolation is why calling run() on the current thread is dangerous in concurrent programs: there is no stack isolation — the current thread’s stack is used, and any shared state mutations happen without the concurrency guarantees you expected from a separate thread.

Virtual Threads (Java 21+)

Java 21 introduced virtual threads via Project Loom. The same start() / run() distinction applies — you still call start() to create a virtual thread, and you still must not call run() directly if you want concurrency. The difference is that virtual threads are managed by the JVM scheduler, not the OS, so you can create millions of them cheaply.

// Java 21+: virtual thread — same rule applies
Thread vt = Thread.ofVirtual().name("my-vthread").unstarted(
    () -> System.out.println("Running on: " + Thread.currentThread())
);
vt.start(); // correct — spawns a virtual thread
// vt.run() — wrong — would run on the current platform thread

See Virtual Threads for a full deep-dive.


Quick Reference

Thread t = new Thread(() -> System.out.println(
    Thread.currentThread().getName()
));

t.run();   // prints "main"    — no new thread
t.start(); // prints "Thread-0" — new thread created
run()start()
Creates a new threadNoYes
Thread state changesNo (stays NEW)Yes (NEW → RUNNABLE → …)
Can be called multiple timesYesNo (2nd call throws)
Involves a system callNoYes
Suitable for concurrencyNoYes

  • Creating a Thread — The three ways to create a thread and when to choose each approach.
  • Thread Life Cycle — All states a thread passes through, from NEW to TERMINATED, and what triggers each transition.
  • Starting a Thread Twice — Why calling start() more than once throws IllegalThreadStateException.
  • Thread Pools & Executors — Avoid the overhead of creating raw threads by reusing them with ExecutorService.
  • Multithreading — The big picture of Java concurrency: concepts, tools, and common patterns.
  • Virtual Threads (Project Loom) — Java 21’s lightweight thread model that keeps the same start() API but scales to millions of threads.
Last updated June 13, 2026
Was this helpful?