Concurrency Basics

The Thread Lifecycle

15 min Lesson 3 of 13

The Thread Lifecycle

Understanding what a thread is doing at any point in time is essential for writing correct concurrent code. Java models a thread's existence as a finite-state machine: a thread moves through a well-defined set of states from the moment it is created until it terminates. Knowing these states — and the methods that trigger transitions between them — lets you reason about program behaviour, diagnose hangs, and write coordination logic that actually works.

The Six Thread States

The Thread.State enum (introduced in Java 5, still current in Java 17+) defines six states:

  • NEW — A Thread object has been created but start() has not yet been called. No operating-system thread exists yet.
  • RUNNABLE — The thread has been started. It is either running on a CPU core right now, or it is ready to run and waiting for the OS scheduler to give it a core. Java does not distinguish between "running" and "ready-to-run" in the state enum.
  • BLOCKED — The thread is waiting to acquire an intrinsic monitor lock (a synchronized block or method held by another thread). It cannot proceed until the lock is released.
  • WAITING — The thread is indefinitely suspended, waiting for another thread to take a specific action. This happens when you call Object.wait(), Thread.join() with no timeout, or LockSupport.park().
  • TIMED_WAITING — Like WAITING but with a maximum duration. Methods that cause this: Thread.sleep(millis), Object.wait(millis), Thread.join(millis), LockSupport.parkNanos().
  • TERMINATED — The thread's run() method has returned, or an unhandled exception ended it. The Thread object still exists in memory but can never be restarted.
BLOCKED vs WAITING: Both mean the thread is suspended, but the cause is different. BLOCKED is always about acquiring a monitor lock. WAITING and TIMED_WAITING are about coordination signals — the thread voluntarily released the lock and is waiting for a notification or a timeout.

You can inspect a thread's current state at runtime with thread.getState(). This is mostly useful for diagnostics and tooling, not for control flow.

sleep — Pausing the Current Thread

Thread.sleep(long millis) causes the currently executing thread to enter TIMED_WAITING for at least the specified number of milliseconds, then return to RUNNABLE. It is a static method — you cannot make another thread sleep.

public class SleepDemo { public static void main(String[] args) throws InterruptedException { System.out.println("Starting work..."); Thread.sleep(2_000); // sleep 2 seconds System.out.println("Resumed after sleep."); } }

Two important facts about sleep:

  • It does not release monitor locks. If the sleeping thread holds a synchronized lock, other threads waiting for that lock remain blocked for the entire duration of the sleep. This is a common source of poor throughput.
  • The duration is a minimum, not a guarantee. The OS scheduler may resume the thread slightly later than requested, especially on a heavily loaded system.
public class SleepKeepsLock { private static final Object LOCK = new Object(); public static void main(String[] args) throws InterruptedException { Thread holder = new Thread(() -> { synchronized (LOCK) { System.out.println("Holder: acquired lock, sleeping..."); try { Thread.sleep(3_000); // lock is NOT released during sleep } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Holder: done."); } }); Thread waiter = new Thread(() -> { System.out.println("Waiter: trying to acquire lock (BLOCKED)..."); synchronized (LOCK) { System.out.println("Waiter: finally got the lock."); } }); holder.start(); Thread.sleep(100); // let holder grab the lock first waiter.start(); } }

Run this and you will see the waiter stay BLOCKED for the full 3 seconds.

join — Waiting for Another Thread to Finish

thread.join() causes the calling thread to enter WAITING (or TIMED_WAITING with a timeout argument) until thread reaches TERMINATED. It is the standard way to wait for background work to complete before using its results.

public class JoinDemo { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { System.out.println("Worker: computing..."); try { Thread.sleep(1_500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Worker: done."); }); worker.start(); System.out.println("Main: waiting for worker..."); worker.join(); // main enters WAITING here System.out.println("Main: worker finished, continuing."); } }

The timed variant worker.join(millis) returns after the timeout even if the worker is still running — always check worker.isAlive() afterward if the result matters.

Joining a list of threads: Iterate and call join() on each. The total wall-clock wait is roughly the duration of the longest task, not the sum — all tasks run in parallel, and the loop simply waits for each one in sequence.

interrupt — Requesting Cancellation

Java's cancellation model is cooperative. You do not forcibly stop a thread; you set its interrupt flag with thread.interrupt() and the thread is expected to notice and stop voluntarily.

There are two ways a thread observes the interrupt flag:

  • Blocking methods throw InterruptedException. When a thread is in WAITING or TIMED_WAITING (sleeping, joining, or waiting on a monitor via Object.wait()) and another thread calls interrupt() on it, the blocking call throws InterruptedException and the interrupt flag is cleared.
  • Polling with Thread.interrupted() or thread.isInterrupted(). Long-running CPU-bound loops can check the flag periodically.
public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { int count = 0; while (!Thread.currentThread().isInterrupted()) { count++; if (count % 1_000_000 == 0) { System.out.println("Working... " + count); } } System.out.println("Worker: interrupt flag set, stopping cleanly."); }); worker.start(); Thread.sleep(50); // let it run briefly worker.interrupt(); // request cancellation worker.join(); System.out.println("Main: worker has stopped."); } }
Never swallow InterruptedException silently. The pattern catch (InterruptedException e) { /* ignore */ } clears the interrupt flag without acting on it, making the thread unresponsive to cancellation. Either re-throw the exception (if your method signature allows it), or restore the flag with Thread.currentThread().interrupt() before returning.

The correct re-set pattern:

try { Thread.sleep(1_000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore the flag return; // then stop cleanly }

Putting It Together: A State Transition Walkthrough

Consider this sequence for a single thread:

  1. new Thread(task) — state is NEW.
  2. thread.start() — JVM creates an OS thread; state becomes RUNNABLE.
  3. Inside task, calls Thread.sleep(500) — state becomes TIMED_WAITING.
  4. Sleep expires — state returns to RUNNABLE.
  5. Task tries to enter a synchronized block held by another thread — state becomes BLOCKED.
  6. Lock is released — state returns to RUNNABLE.
  7. run() returns — state becomes TERMINATED.
public class LifecycleInspector { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); System.out.println("After new: " + t.getState()); // NEW t.start(); System.out.println("After start: " + t.getState()); // RUNNABLE Thread.sleep(100); System.out.println("While sleep: " + t.getState()); // TIMED_WAITING t.join(); System.out.println("After join: " + t.getState()); // TERMINATED } }

Summary

A Java thread progresses through six well-defined states: NEW → RUNNABLE → (BLOCKED / WAITING / TIMED_WAITING) → TERMINATED. The key methods governing these transitions are Thread.sleep() (voluntary pause without releasing locks), Thread.join() (wait for another thread to finish), and Thread.interrupt() (cooperative cancellation via flag). Mastering these transitions is the foundation for every higher-level concurrency construct covered in the rest of this tutorial.