Java Thread Lifecycle

In the last lesson you learned about the Java Runnable interface. Now let’s follow the thread itself, from birth to death. A thread does not just run and stop. It moves through a handful of named states during its life, and knowing them is the difference between guessing why your threads misbehave and actually understanding it.

πŸ€” Why should you care about thread states?

Picture a worker. Sometimes they are hired but not at the desk, sometimes working, sometimes stuck waiting for a key, sometimes on a break, sometimes gone home for good. You would never assume a worker is busy just because they are employed. Same with threads.

Here is the real pain this solves:

  • Your program freezes and you have no idea why.
  • One thread is stuck waiting for a lock another thread never releases.
  • You call a method on a thread that already finished and nothing happens.

If you cannot name a thread’s state, you cannot debug these. Java names the states in an enum called Thread.State. There are exactly six, and we cover each one.

🧩 The six thread states

Every Java thread is always in exactly one of six states:

  • NEW β€” the thread object is created, but you have not called start() yet. It is hired but not at the desk.
  • RUNNABLE β€” start() was called. The thread is either running right now or ready to run and waiting for its turn.
  • BLOCKED β€” the thread wants a lock that another thread is holding. It is stuck at a locked door.
  • WAITING β€” the thread is waiting for another thread to do something, with no time limit. It waits until told to continue.
  • TIMED_WAITING β€” same idea, but with a deadline. It waits for a set time, then gives up waiting on its own.
  • TERMINATED β€” the thread’s work is done. The run() method returned, or the thread was stopped. It has gone home for good.

A thread is always somewhere on this map. Things you do, like start(), sleep(), or wait(), move it from one spot to another.

πŸ—ΊοΈ A map of the states

Here is how a thread travels between the states.

start

wants a held lock

gets the lock

wait or join

notify or thread ends

sleep or wait with time

time is up

run returns

NEW

RUNNABLE

BLOCKED

WAITING

TIMED_WAITING

TERMINATED

Read it like a journey. A thread starts at NEW, enters RUNNABLE on start(), can detour into BLOCKED, WAITING, or TIMED_WAITING, but always returns to RUNNABLE before it finishes. When run() returns, it lands in TERMINATED and stays there. There is no road out. A finished thread cannot be restarted.

🌱 NEW: created but not started

A thread begins life in NEW. You built the Thread object, but no path of execution exists yet. It is just an object in memory.

This program creates a thread and checks its state before starting it.

public class Main {
public static void main(String[] args) {
Thread worker = new Thread(() -> System.out.println("Working"));
// we built the thread but have not started it
System.out.println("State: " + worker.getState());
}
}

Here is what is happening:

  • new Thread(...) builds the thread object and gives it a task.
  • We have not called start(), so no separate thread is running.
  • getState() asks the thread what state it is in. It returns a value from the Thread.State enum.

Output

State: NEW

A NEW thread has never run and is not scheduled. Calling start() is the only thing that moves it forward.

πŸƒ RUNNABLE: started and ready

Once you call start(), the thread enters RUNNABLE. This is the most misread state. RUNNABLE does not mean β€œrunning right now.” It means β€œable to run.” The thread is either using a CPU core this instant, or sitting in a ready line waiting for its turn.

This demo checks the state right after start().

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i;
}
});
worker.start(); // move from NEW to RUNNABLE
System.out.println("State: " + worker.getState());
}
}

Let’s break it down:

  • worker.start() creates the real thread and puts it in RUNNABLE.
  • The task is just adding numbers in a loop, which keeps it busy.
  • We print the state straight after starting, so we usually catch it as RUNNABLE.

Output

State: RUNNABLE

Why β€œusually”? You do not control exactly when the operating system runs the thread. On a fast machine with a tiny task, it could even finish before this line prints. You do not get precise control over the scheduler.

⏳ TIMED_WAITING: waiting with a deadline

When a thread calls Thread.sleep(ms), it steps aside for a set time. During that pause it is in TIMED_WAITING. It is not using the CPU, just resting until the clock runs out, then it returns to RUNNABLE on its own.

This program starts a thread that sleeps, then checks its state from the main thread.

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread sleeper = new Thread(() -> {
try {
Thread.sleep(2000); // pause for 2 seconds
} catch (InterruptedException e) {
// ignored for this demo
}
});
sleeper.start();
Thread.sleep(200); // give it a moment to start sleeping
System.out.println("State: " + sleeper.getState());
}
}

Here is the flow:

  • The sleeper thread starts and calls Thread.sleep(2000), which pauses it for two seconds.
  • The main thread sleeps a short moment first, so the sleeper has time to enter its own sleep.
  • Then main prints the sleeper’s state, which catches it mid-sleep.

Output

State: TIMED_WAITING

The same TIMED_WAITING state happens with wait(ms) and join(ms). They all say β€œwait, but only for this long.” When the time is up, the thread returns to RUNNABLE by itself.

πŸ›‘ WAITING: waiting with no deadline

WAITING is like TIMED_WAITING but with no timer. The thread waits until another thread acts. A common way to reach it is join() with no argument, meaning β€œwait for that thread to finish, however long it takes.” It also happens with wait() and no time value.

This program has main call join() on a worker, so main itself enters WAITING.

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
Thread.sleep(3000); // worker stays busy for a while
} catch (InterruptedException e) {
// ignored for this demo
}
});
Thread checker = new Thread(() -> {
// print the main thread's state while it waits
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println("Main state: " +
Thread.currentThread().getThreadGroup());
});
worker.start();
Thread main = Thread.currentThread();
Thread observer = new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println("Main is: " + main.getState());
});
observer.start();
worker.join(); // main waits here, with no time limit
}
}

Let’s walk through the important bit:

  • worker.join() tells the main thread to wait until worker finishes.
  • There is no time value, so main waits as long as it takes. That is WAITING, not TIMED_WAITING.
  • The observer thread peeks at main’s state while main is parked at join().

Output

Main is: WAITING

The difference is just the deadline. WAITING has none, so the thread cannot wake itself. Something else, like the other thread finishing or a notify() call, has to release it.

πŸ”’ BLOCKED: stuck on a lock

A thread enters BLOCKED when it wants to enter a synchronized section but another thread is already inside holding the lock. Think of one bathroom with one key. The second person waits at the door until the key is free.

This program makes two threads fight over the same lock so we can catch one in BLOCKED.

public class Main {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
synchronized (lock) { // only one thread at a time
try {
Thread.sleep(2000); // hold the lock for a while
} catch (InterruptedException e) {
}
}
};
Thread first = new Thread(task);
Thread second = new Thread(task);
first.start();
Thread.sleep(200); // let first grab the lock
second.start();
Thread.sleep(200); // let second hit the locked door
System.out.println("Second is: " + second.getState());
}
}

Here is the sequence:

  • first starts, enters the synchronized block, grabs the lock, and sleeps while holding it.
  • second starts a moment later and tries to enter the same block.
  • The lock is taken, so second is stuck at the door. That is BLOCKED.

Output

Second is: BLOCKED

BLOCKED means β€œI want a lock someone else holds.” WAITING means β€œI am waiting for another thread to signal me.” Both are paused, but for different reasons. You will meet locks properly in the synchronization lesson.

πŸ’€ TERMINATED: the thread is done

When run() finishes and returns, the thread reaches TERMINATED. Its work is over and it never runs again. A terminated thread cannot be restarted. Calling start() on it throws an error.

This program lets a quick thread finish, waits for it, then checks its state.

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> System.out.println("Done quickly"));
worker.start();
worker.join(); // wait for it to fully finish
System.out.println("State: " + worker.getState());
}
}

Here is what happens:

  • The thread prints one line and its run() returns right away.
  • worker.join() makes main wait until the thread is fully finished.
  • After that, the thread is guaranteed to be TERMINATED.

Output

Done quickly
State: TERMINATED

Once a thread is TERMINATED, that is the end of the road. If you need the work again, create a fresh Thread.

🎬 Watching one thread change states

This program follows a single thread and prints its state at several points in its life. It is the clearest way to see the whole journey at once.

public class Main {
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
Thread.sleep(500); // enters TIMED_WAITING
} catch (InterruptedException e) {
}
});
System.out.println("After create: " + worker.getState());
worker.start();
System.out.println("After start: " + worker.getState());
Thread.sleep(100); // let it reach its sleep
System.out.println("While sleep: " + worker.getState());
worker.join(); // wait until it finishes
System.out.println("After join: " + worker.getState());
}
}

Let’s trace the state at each print:

  • Before start(), the thread is NEW.
  • Right after start(), it is RUNNABLE.
  • While it is inside Thread.sleep, it is TIMED_WAITING.
  • After join() confirms it finished, it is TERMINATED.

Output

After create: NEW
After start: RUNNABLE
While sleep: TIMED_WAITING
After join: TERMINATED

That single run shows four of the six states in order. Run small experiments like this to build a feel for how threads move.

⚠️ Common Mistakes

A few thread-state misunderstandings cause real bugs and confusion. Here is how to avoid them.

Thinking RUNNABLE means β€œrunning right now.” It does not. RUNNABLE means the thread is able to run, which includes sitting in line waiting for a CPU turn.

// ❌ wrong assumption: "RUNNABLE means it is using the CPU this instant"
if (t.getState() == Thread.State.RUNNABLE) {
// this thread might be running OR just waiting for its turn
}
// βœ… correct understanding: RUNNABLE = running or ready to run
// Java does not have a separate "RUNNING" state at all

Expecting precise control over scheduling. You cannot force the operating system to run a thread at an exact moment. State checks are a snapshot, and the picture can change a breath later.

// ❌ fragile: assuming the state stays put after you read it
Thread.State s = t.getState();
// by the next line, s may already be out of date
// βœ… use getState() for debugging and learning, not for control flow
// to coordinate threads, use join(), wait()/notify(), or higher-level tools

Trying to restart a TERMINATED thread. A finished thread is done for good. Calling start() again throws IllegalThreadStateException.

Thread t = new Thread(() -> System.out.println("hi"));
t.start();
// ❌ a thread can be started only once
// t.start(); // throws IllegalThreadStateException
// βœ… make a brand new thread when you need the work again
new Thread(() -> System.out.println("hi")).start();

Confusing BLOCKED with WAITING. They look similar in a debugger but mean different things. BLOCKED is about a lock. WAITING is about a signal from another thread. Naming the right one points you at the right fix.

βœ… Best Practices

These habits keep your understanding of threads sharp and your code reliable.

  • Use getState() for learning and debugging, not for control. It is a great window into what a thread is doing. It is a poor tool for coordinating threads, because the state can change the instant after you read it.
  • Coordinate with the right tools. Use join() to wait for a thread to finish, and wait() with notify() for signaling. Do not poll getState() in a loop to wait for something.
  • Remember a thread runs only once. When you need the same work again, create a fresh Thread rather than reaching for a finished one.
  • Do not assume an order. The scheduler decides who runs when. Build coordination into your code instead of hoping the timing works out.
  • Match the state to the cause. When debugging a freeze, read the state. BLOCKED points to a lock; WAITING points to a missing signal; TIMED_WAITING points to a sleep or timeout.

There is no RUNNING state

A common surprise is that Java has no separate RUNNING state. A thread that is actively using a CPU and a thread that is ready and waiting for its turn are both reported as RUNNABLE. The operating system handles the in-and-out of the CPU below what Thread.State shows you.

🧩 What You’ve Learned

Great work. Let’s recap the thread lifecycle.

  • βœ… A thread is always in exactly one of six states from the Thread.State enum.
  • βœ… NEW is created but not started; start() moves it to RUNNABLE.
  • βœ… RUNNABLE means running or ready to run. There is no separate RUNNING state.
  • βœ… BLOCKED is waiting for a lock another thread holds.
  • βœ… WAITING is waiting with no time limit; TIMED_WAITING is waiting with a deadline, like sleep(ms).
  • βœ… TERMINATED means run() returned; a finished thread cannot be restarted.
  • βœ… You can read the current state with getState(), which is best used for learning and debugging.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What state is a thread in right after you create it but before start()?

    Why: A freshly created Thread object that has not been started is in the NEW state.

  2. 2

    What does the RUNNABLE state mean?

    Why: RUNNABLE covers both actively running and ready-and-waiting-for-a-turn. Java has no separate RUNNING state.

  3. 3

    Which state does Thread.sleep(1000) put a thread in?

    Why: sleep(ms) pauses the thread for a set time, which is TIMED_WAITING.

  4. 4

    What happens if you call start() on a TERMINATED thread?

    Why: A thread can be started only once; starting a finished thread throws IllegalThreadStateException.

πŸš€ What’s Next?

You saw BLOCKED happen when threads fight over a lock, and you saw race conditions hinted at earlier. The tool that makes shared access safe is synchronization. Let’s learn it, because it is the key to correct multithreaded code.

Java Synchronization

Share & Connect