Starting a Thread Twice
Once a thread finishes its job, it’s done for good — you cannot restart it by calling start() again. Attempting to do so throws an IllegalThreadStateException at runtime. Understanding why this happens helps you write safer, more predictable concurrent code.
What Happens When You Call start() Twice
Every Thread object moves through a fixed set of states during its life. When you call start() the first time, the thread transitions from NEW → RUNNABLE, executes its run() method, and eventually reaches the TERMINATED state. At that point, the thread is finished — calling start() again throws an exception because the JVM rejects transitioning out of the TERMINATED state.
public class StartTwiceDemo extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
StartTwiceDemo t = new StartTwiceDemo();
t.start(); // First call — works fine
t.join(); // Wait for the thread to finish
t.start(); // Second call — throws IllegalThreadStateException!
}
}
Output:
Thread running: Thread-0
Exception in thread "main" java.lang.IllegalThreadStateException
at java.base/java.lang.Thread.start(Thread.java:794)
at StartTwiceDemo.main(StartTwiceDemo.java:13)
Warning: This exception is thrown even if the thread has already finished. A terminated thread cannot be restarted under any circumstances.
Why Java Forbids Restarting a Thread
The Thread Life Cycle in Java is a one-way journey. A thread progresses from NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED, and there is no path from TERMINATED back to any other state.
Here’s what the lifecycle looks like in code terms:
public class ThreadStateExample extends Thread {
@Override
public void run() {
System.out.println("State while running: " + getState()); // RUNNABLE
}
public static void main(String[] args) throws InterruptedException {
ThreadStateExample t = new ThreadStateExample();
System.out.println("Before start: " + t.getState()); // NEW
t.start();
t.join();
System.out.println("After finish: " + t.getState()); // TERMINATED
}
}
Output:
Before start: NEW
State while running: RUNNABLE
After finish: TERMINATED
Note:
Thread.getState()returns aThread.Stateenum value. You can use this to inspect a thread’s current state programmatically.
The Right Way: Create a New Thread Instance
The simplest fix is to create a fresh Thread object each time you want the work to run again. Since the work logic lives in the Runnable, you can reuse that easily.
public class ReuseRunnableDemo {
static Runnable task = () ->
System.out.println("Running in: " + Thread.currentThread().getName());
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(task, "worker-1");
t1.start();
t1.join();
// Don't restart t1 — create a new thread with the same Runnable
Thread t2 = new Thread(task, "worker-2");
t2.start();
t2.join();
}
}
Output:
Running in: worker-1
Running in: worker-2
Separating your logic (the Runnable) from the thread itself is a clean design habit that naturally avoids this pitfall. See Creating a Thread for the full details on both Thread subclassing and the Runnable approach.
Using a Thread Pool to Reuse Workers
For production code, you rarely manage individual Thread objects directly. A Thread Pool (via ExecutorService) handles creation and reuse automatically — you just submit tasks.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Runnable task = () ->
System.out.println("Task by: " + Thread.currentThread().getName());
// Submit the same logical task multiple times — no IllegalThreadStateException
pool.submit(task);
pool.submit(task);
pool.submit(task);
pool.shutdown(); // signals the pool to stop accepting new work
}
}
Output:
Task by: pool-1-thread-1
Task by: pool-1-thread-2
Task by: pool-1-thread-1
Tip: Always prefer
ExecutorServiceover rawThreadmanagement in real applications. It handles thread reuse, exception handling, and shutdown gracefully.
Guarding Against Accidental Double-Start
If you ever build a component that wraps a Thread internally, add a guard to prevent double-start bugs at the design level:
public class ManagedWorker {
private Thread thread;
private volatile boolean started = false;
public synchronized void start() {
if (started) {
throw new IllegalStateException("Worker already started. Create a new instance.");
}
started = true;
thread = new Thread(this::doWork, "managed-worker");
thread.start();
}
private void doWork() {
System.out.println("Working in: " + Thread.currentThread().getName());
// ... actual work here
}
public static void main(String[] args) {
ManagedWorker worker = new ManagedWorker();
worker.start(); // fine
// worker.start(); // would throw IllegalStateException with a helpful message
}
}
Using synchronized and the started flag ensures no two callers accidentally start the same worker concurrently either.
Under the Hood
When you call t.start(), the JVM calls the native method Thread.start0(). Internally, before doing anything else, the runtime checks the thread’s threadStatus field (a C-level integer). If threadStatus != 0 — meaning the thread has already been started at some point — the JVM immediately throws IllegalThreadStateException without creating a new OS thread.
This check is intentionally strict: Java does not attempt to “reset” a thread because doing so would create race conditions (what happens to the thread’s local variables, interrupt status, or daemon flag mid-execution?). A clean new Thread object gives you a predictable, zeroed-out starting state every time.
Thread state transitions are also why calling start() on a thread that is merely BLOCKED or WAITING throws the same exception — those states are also non-NEW states, even if the thread hasn’t finished yet.
Note: Checking
t.getState() == Thread.State.NEWbefore callingstart()is a useful defensive pattern when you receive aThreadreference from external code and aren’t sure of its state.
Quick Reference
| Scenario | Result |
|---|---|
Call start() on a NEW thread | Works — thread begins execution |
Call start() on a RUNNABLE thread | IllegalThreadStateException |
Call start() on a TERMINATED thread | IllegalThreadStateException |
Call run() directly (instead of start()) | Runs on the current thread, no new thread created |
Create a new Thread with same Runnable | Works — fresh thread, correct behavior |
Tip: Calling
run()directly is not the same as starting a thread — it executes the method synchronously on the calling thread. Always usestart()when you want actual concurrency. See run() vs start() for the full comparison.
Related Topics
- Thread Life Cycle — understand every state a thread passes through before termination
- Creating a Thread — the two standard ways to create threads and when to choose each
- run() vs start() — why calling
run()directly is a silent concurrency bug - Thread Pool & Executors — the production-ready alternative to managing raw Thread objects
- Thread.sleep() — control thread timing without busy-waiting
- Joining Threads — wait for a thread to finish before proceeding