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
| Method | What it does | New thread created? |
|---|---|---|
start() | Asks the JVM to spawn a new OS thread, then calls run() on it | Yes |
run() | Executes the method body on the current thread like any normal method call | No |
Warning: Calling
run()directly is not an error — it just silently executes your task on the thread that made the call (usuallymain). 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 calledrun()instead ofstart().
When Is It Valid to Call run() Directly?
Rarely, but it does happen:
- Unit testing a
RunnableorThreadsubclass’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:
- Validates the thread is in
NEWstate — throwsIllegalThreadStateExceptionotherwise. - Allocates a native OS thread and a per-thread call stack (default size varies; tunable with
-Xss). - Transitions the
Threadobject’s state toRUNNABLE. - Registers the thread with the JVM’s internal thread table.
- 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 constructor —
Thread.run()delegates totarget.run(). - Thread subclass with overridden
run()— the override replaces the method entirely;targetis 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 thread | No | Yes |
| Thread state changes | No (stays NEW) | Yes (NEW → RUNNABLE → …) |
| Can be called multiple times | Yes | No (2nd call throws) |
| Involves a system call | No | Yes |
| Suitable for concurrency | No | Yes |
Related Topics
- 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 throwsIllegalThreadStateException. - 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.