PrepZone Logo
PrepZone

Threads and Their Lifecycle

Creating threads three ways, the six states, and what daemon threads change about shutdown.

Read these first

Why this matters

  • The start() versus run() distinction is the most common beginner mistake in Java concurrency, and it fails silently — the code runs, just not concurrently.
  • The six thread states are the vocabulary of every thread dump you will ever read while diagnosing a hang.
  • Daemon threads decide whether your JVM exits, which is a frequent cause of a process that will not shut down.

Three ways to define work

Java
// 1. Extend Thread — uses up your one inheritance slot; rarely the right choice
class Worker extends Thread {
    @Override public void run() {
        System.out.println("running in " + getName());
    }
}
new Worker().start();

// 2. Implement Runnable — separates the task from the thread
class Task implements Runnable {
    @Override public void run() {
        System.out.println("running in " + Thread.currentThread().getName());
    }
}
new Thread(new Task()).start();

// 3. A lambda — Runnable is a functional interface
new Thread(() -> System.out.println("running")).start();
Aspectextends Threadimplements Runnable
InheritanceUses the single extends slotLeaves it free
SeparationTask and thread are one objectThe task is independent of how it runs
ReusableA Thread cannot be restartedThe same Runnable can be submitted many times
Works with executorsAwkwardlyDirectly
RecommendedNoYes
  • Inheritance

    extends ThreadUses the single extends slot
    implements RunnableLeaves it free
  • Separation

    extends ThreadTask and thread are one object
    implements RunnableThe task is independent of how it runs
  • Reusable

    extends ThreadA Thread cannot be restarted
    implements RunnableThe same Runnable can be submitted many times
  • Works with executors

    extends ThreadAwkwardly
    implements RunnableDirectly
  • Recommended

    extends ThreadNo
    implements RunnableYes

Prefer Runnable. In practice, prefer an executor over creating threads at all.

The six states

NEWCreated, start() not yet called
RUNNABLEEligible for the CPU
BLOCKEDWaiting for a monitor lock
WAITINGwait() / join()
TIMED_WAITINGsleep(ms)
TERMINATEDrun() returned
A thread only runs in RUNNABLE. Blocked, waiting and timed-waiting are three different reasons for not running, and knowing which one a thread is in tells you where to look.
  • NEW — constructed but start() has not been called.
  • RUNNABLE — eligible to run. The thread may be executing or waiting for a CPU; Java does not distinguish.
  • BLOCKED — waiting to acquire a monitor lock held by another thread.
  • WAITING — waiting indefinitely for another thread to act: wait(), join(), LockSupport.park().
  • TIMED_WAITING — the same, with a deadline: sleep(ms), wait(ms), join(ms), poll(timeout).
  • TERMINATED — run() has completed, normally or by throwing.
Java
Thread.State state = thread.getState();

These names are exactly what appears in a thread dump, which is why they are worth memorising:

Essential methods

Java
Thread.sleep(1000);                    // static: pauses the CURRENT thread, keeps any locks held
thread.join();                          // waits for `thread` to terminate
thread.join(500);                       // waits at most 500 ms
thread.interrupt();                     // sets the interrupt flag; a request, not a kill
Thread.currentThread().isInterrupted(); // checks without clearing
Thread.interrupted();                   // checks AND clears — easy to misuse
Thread.yield();                         // a hint to the scheduler; no guarantee
thread.setDaemon(true);                 // must be called before start()
thread.setName("worker-1");             // do this; it transforms thread dumps
thread.setPriority(Thread.NORM_PRIORITY);

Interruption is cooperative

Java has no way to forcibly stop a thread. stop() was deprecated decades ago because it could terminate a thread mid-update, leaving shared state inconsistent. Instead, interruption sets a flag that the thread is expected to notice.

Java
public class Worker implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                doWork();
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();   // restore the flag the catch cleared
                break;                                 // and actually stop
            }
        }
        cleanUp();
    }
}

The rules

  • interrupt() sets a flag. A thread blocked in sleep, wait or join wakes with InterruptedException.
  • Catching InterruptedException clears the flag. If you neither rethrow nor restore it, code above you will never learn about the request.
  • A thread in a tight computational loop must check isInterrupted() itself — nothing will interrupt it.
  • Swallowing InterruptedException is how an executor shutdown hangs forever.

Daemon threads

A daemon thread does not prevent the JVM from exiting. When only daemon threads remain, the JVM terminates immediately.

Java
Thread background = new Thread(() -> {
    while (true) {
        collectMetrics();
        sleepQuietly(60_000);
    }
});
background.setDaemon(true);     // must be set before start()
background.start();
AspectUser threadDaemon thread
Keeps the JVM aliveYesNo
On JVM exitThe JVM waits for itKilled abruptly, no finally blocks
DefaultYes — inherits from the creating threadMust be set explicitly
Suitable forWork that must completeBackground monitoring, metrics, housekeeping
  • Keeps the JVM alive

    User threadYes
    Daemon threadNo
  • On JVM exit

    User threadThe JVM waits for it
    Daemon threadKilled abruptly, no finally blocks
  • Default

    User threadYes — inherits from the creating thread
    Daemon threadMust be set explicitly
  • Suitable for

    User threadWork that must complete
    Daemon threadBackground monitoring, metrics, housekeeping

A daemon thread is terminated without cleanup, so never use one for work that must finish.

The most common symptom of getting this wrong: a main method finishes but the process stays alive, because a non-daemon thread somewhere is still running. The executor you forgot to shut down is usually the culprit.

Uncaught exceptions

An exception escaping run() terminates only that thread. By default it prints a trace to standard error, and nothing else notices.

Java
Thread worker = new Thread(() -> {
    throw new IllegalStateException("failed");
});

worker.setUncaughtExceptionHandler((thread, throwable) ->
        log.error("Thread {} died", thread.getName(), throwable));

worker.start();

// Or a default for every thread without its own handler
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) ->
        log.error("Uncaught in {}", thread.getName(), throwable));

Without a handler, a silently dying background thread is one of the harder failures to notice: the application keeps serving requests while one piece of it has quietly stopped working.

Prefer an executor

Creating threads directly is almost always the wrong level of abstraction.

Java
// Unbounded and manual
for (Task task : tasks) {
    new Thread(task).start();         // 10,000 tasks means 10,000 OS threads
}

// Bounded, reusable, with lifecycle management
try (ExecutorService pool = Executors.newFixedThreadPool(8)) {     // Java 19+ closes it
    for (Task task : tasks) {
        pool.submit(task);
    }
}

Why

  • Thread creation is expensive — around 1 MB of stack each, plus an OS-level operation.
  • Unbounded creation exhausts memory. Each thread's stack is real memory outside the heap.
  • Executors reuse threads, queue work, and give you a shutdown path and failure reporting.
  • Virtual threads change this calculus for blocking workloads, and are covered in the Java 21 module.

Common misreadings

  • "run() starts a thread." It executes the body on the current thread. Only start() creates one.
  • "A thread can be restarted." start() on a terminated thread throws. Create a new one.
  • "sleep() releases locks." It does not. Only wait() does.
  • "interrupt() stops a thread." It sets a flag. The thread must cooperate.
  • "RUNNABLE means executing." It means eligible to execute. The OS may not have scheduled it.
  • "Thread priorities control scheduling." They are a hint, honoured inconsistently across platforms. Do not design around them.
  • "An uncaught exception stops the application." It stops that thread, often invisibly.

Quick recall

Everything you need if you only revisit this box.

  • Prefer Runnable (or a lambda) over extending Thread, and prefer an executor over either.
  • start() creates a thread; run() just calls a method. A thread is single-use.
  • Six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — the vocabulary of every thread dump.
  • sleep() keeps its locks; only wait() releases them.
  • Interruption is cooperative: catching InterruptedException clears the flag, so rethrow or call Thread.currentThread().interrupt().
  • Daemon threads do not keep the JVM alive and are killed without cleanup. Set the flag before start().
  • An uncaught exception kills only that thread — install an UncaughtExceptionHandler.
  • Name your threads. It is the difference between a readable and an unreadable thread dump.

Test yourself

Answer these before moving on — recall is what makes it stick.