Definition
- start() – creates a new OS-level thread, adds it to the thread group, and the JVM calls
run()on that new thread. The calling thread continues in parallel immediately afterstart()returns. - run() – when called directly, it is just an ordinary method invocation on the current thread. No new thread is created. The calling thread is blocked until
run()completes. There is no concurrency. - Single use –
start()can only be called once perThreadobject. The thread must be in the NEW state. Callingstart()on a RUNNABLE or TERMINATED thread throwsIllegalThreadStateException. - Native mechanism – internally,
start()calls the native methodstart0(), which invokes the OS thread creation API (pthread_createon Linux,CreateThreadon Windows).
start() is like hiring a new teller and telling them to go serve customers at their own counter. The new teller works in parallel with you. run() is like you walking over to the other counter and doing the work yourself. No new teller was hired. You left your own counter, did the work sequentially, and came back. There is no parallelism.
Understanding the Concepts
What does start() do?
When you call start() on a Thread object, the JVM performs the following steps:
- Checks that the thread is in the NEW state. If not, throws
IllegalThreadStateException. - Adds the thread to its
ThreadGroup. - Calls the native
start0()method, which requests the OS to create a new thread. - The new OS thread calls
this.run(). - The calling thread returns from
start()immediately, without waiting forrun()to complete.
package com.bank.core;
public class StartDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("Task on: " + Thread.currentThread().getName());
}, "SettlementWorker");
t.start(); // new OS thread created, runs in parallel
System.out.println("Main continues on: " + Thread.currentThread().getName());
}
}
Main continues on: main
Task on: SettlementWorker
The output order may vary because both threads run in parallel.
What does calling run() directly do?
Calling run() directly is just an ordinary method call. The method executes on the current thread. No new thread is created.
package com.bank.core;
public class RunDirectlyDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("Task on: " + Thread.currentThread().getName());
}, "SettlementWorker");
t.run(); // NO new thread! Runs on main thread.
System.out.println("Main continues on: " + Thread.currentThread().getName());
}
}
Task on: main
Main continues on: main
Both print statements show "main" because run() executed on the main thread. The output order is always the same because there is no concurrency.
How do the two compare?
| Feature | start() | run() called directly |
|---|---|---|
| New thread created | Yes | No |
| Execution thread | New OS thread | Current thread |
| Parallelism | Yes | No |
| Thread state change | NEW to RUNNABLE | No state change |
| Can call twice | No (IllegalThreadStateException) | Yes (just a method call) |
Why can start() only be called once?
The first call to start() transitions the thread from NEW to RUNNABLE. After run() completes, the thread enters TERMINATED. Neither RUNNABLE nor TERMINATED can transition back to NEW. Calling start() again would violate the lifecycle contract.
package com.bank.core;
public class DoubleStartDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> System.out.println("Processing trade"));
t.start();
t.join();
try {
t.start(); // TERMINATED thread cannot restart
} catch (IllegalThreadStateException e) {
System.out.println("Cannot restart terminated thread");
}
}
}
Processing trade
Cannot restart terminated thread
What is the real-world impact of this bug?
Calling run() instead of start() causes sequential execution instead of parallel execution. In a batch processing system, this can be a severe performance regression.
package com.bank.service;
import java.util.ArrayList;
import java.util.List;
public class BatchProcessor {
public static void main(String[] args) throws InterruptedException {
List<Thread> threads = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
final int batch = i;
Thread t = new Thread(() -> {
try { Thread.sleep(200); } // simulate 200ms of work
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Batch " + batch + " done");
});
threads.add(t);
t.start(); // CORRECT: parallel execution
// t.run(); // WRONG: sequential, 5 x 200ms = 1000ms
}
for (Thread t : threads) t.join();
long elapsed = System.currentTimeMillis() - startTime;
System.out.println("Total time: " + elapsed + "ms");
// With start(): ~200ms (parallel)
// With run(): ~1000ms (sequential)
}
}
Batch 0 done
Batch 1 done
Batch 2 done
Batch 3 done
Batch 4 done
Total time: 210ms
What are common mistakes?
Besides the run-vs-start mistake, another common error is calling start() inside a loop without keeping a reference to join later, leading to fire-and-forget threads that may not complete before the program exits.
// WRONG: no join, main may exit before threads finish
for (int i = 0; i < 10; i++) {
new Thread(() -> settleTradeOrder(order)).start();
}
// Main exits here. Threads may be killed mid-execution.
// CORRECT: collect threads and join
List<Thread> workers = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> settleTradeOrder(order));
workers.add(t);
t.start();
}
for (Thread t : workers) t.join();
What should a senior engineer know?
- start0() native method: on HotSpot,
start0()callsos::create_thread()which callspthread_create()on Linux. The new OS thread runsThread::run()through a JNI callback. - Thread creation cost: each
start()call creates an OS thread with a default 1MB stack. For high-throughput systems, use a thread pool to avoid this per-task cost. - Daemon threads: calling
t.setDaemon(true)beforestart()makes the thread a daemon. The JVM exits when only daemon threads remain, even if they have not finished. Use for background tasks like log flushing.
Visual Overview
start() path:
┌──────────┐ start() ┌──────────────┐ start0() ┌──────────────┐
│ Thread │──────────▶│ JVM internals │──────────▶│ OS creates │
│ (NEW) │ │ check state │ │ new thread │
└──────────┘ │ add to group │ │ calls run() │
└──────────────┘ └──────────────┘
│
Main thread continues │ New thread
immediately (parallel) │ executes run()
▼
run() path:
┌──────────┐ run() ┌──────────────────────────┐
│ Thread │──────────▶│ Executes on CURRENT thread │
│ (NEW) │ │ No new thread created │
└──────────┘ │ Sequential, no parallelism │
└──────────────────────────┘
Key Takeaway
start() creates a new OS thread and calls run() on it, enabling true parallel execution. The calling thread continues immediately. run() called directly is just an ordinary method invocation on the current thread: no new thread, no parallelism, no concurrency. This distinction is one of the most common sources of multithreading bugs in beginner code.
The internal mechanism matters: start() calls the native start0(), which requests the OS to create a thread (via pthread_create on Linux). This has a real cost of about 1ms and 1MB of stack per thread. That is why production code uses thread pools instead of calling start() on raw Thread objects.
Remember one sentence: start() creates a new OS thread for parallel execution, while calling run() directly just runs the method sequentially on the current thread.
---