Java · Java Multithreading

Java: Threads and Their Lifecycle

start versus run, the six thread states and reading a thread dump, interruption and the flag a blocking call clears, uncaught exceptions, daemon threads and ThreadLocal on a pool.

  • 7 min read
  • 12 practice questions
  • 1 graded problem
On this page
  1. Description
  2. What a thread is
  3. Starting a thread
  4. A thread's states
  5. Stopping a thread: interruption
  6. When a thread throws, and daemon threads
  7. ThreadLocal
  8. Common mistakes
  9. Check yourself
  10. Key takeaways
  11. Quick revise
  12. Practice

Description

A web service handles many requests at the same time. A batch job splits a big file across workers. A phone app downloads in the background while you scroll. All of this runs on threads: separate paths of execution inside one program, each doing its own work while sharing the same memory.

Most Java code today doesn't create threads by hand; executors, web servers and frameworks do it for you. But every one of those tools is built on the same few rules about threads, and when something goes wrong (a service that won't shut down, a worker that silently died, one user's data showing up in another user's request), those rules are where the answer is. They're also a favorite topic in interviews for backend roles.

Here's the path, from easiest to most technical:

  1. What a thread is, and how to start one (start versus run).
  2. Waiting for threads, and the states a thread moves through.
  3. Stopping a thread politely: interruption.
  4. What happens when a thread throws, and what "daemon" threads are.
  5. ThreadLocal: data that belongs to one thread, and its trap.

What a thread is

When your program starts, it has one thread, called main. You can start more, and they all run at the same time (on separate CPU cores, or taking turns on one). They share the same objects in memory, which is what makes threads powerful and also what makes them tricky: two threads changing the same data at once can corrupt it. The next skill, on synchronized and volatile, deals with that. This one is about the threads themselves.

Starting a thread

You describe the work as a Runnable (a task with a run() method), give it to a Thread, and call start():

Runnable task = () -> System.out.println("running on " + Thread.currentThread().getName());
Thread t = new Thread(task, "worker");

t.run();      // prints "running on main": just a normal method call, no new thread
t.start();    // prints "running on worker": a new thread runs the task
t.join();     // wait for the worker to finish
t.start();    // IllegalThreadStateException: a thread can only be started once

The first line trips up a lot of people. Calling run() yourself works without any error, but it runs the task on your current thread, so nothing happens in parallel.

A few habits:

  • Pass a Runnable instead of extending Thread. The task stays separate from how it's run, so the same task can later go to a thread pool.
  • In real code, prefer executors (thread pools) over new Thread. They're covered in the Concurrency module.
  • join() waits until a thread finishes. Afterwards, you're guaranteed to see everything that thread wrote.
  • Thread.sleep(ms) pauses the current thread. It doesn't release any lock the thread is holding.

A thread's states

A thread is always in one of six states, which t.getState() reports:

StateWhat it means
NEWcreated, but start() hasn't been called
RUNNABLErunning, or ready to run (this also covers waiting on network I/O)
BLOCKEDwaiting to enter a synchronized block that another thread holds
WAITINGwaiting with no time limit: join(), wait()
TIMED_WAITINGwaiting with a time limit: sleep(ms), join(ms), wait(ms)
TERMINATEDfinished, normally or by throwing

You'll meet these states in a thread dump: a snapshot of every thread in a running program, with its state and what it's doing. When a service hangs, it's the first thing to collect (jstack <pid>). Lots of BLOCKED threads point at a lock everyone is fighting over; lots of WAITING ones at something they're all waiting for that never arrives.

Stopping a thread: interruption

Java has no safe way to force a thread to stop. Instead, you ask it: t.interrupt() sets a flag on the thread. What happens next depends on what the thread is doing:

  • If it's waiting in sleep, wait, join or a blocking queue's take, that call throws an InterruptedException, and the flag is cleared at the same moment.
  • Otherwise nothing happens until the thread checks Thread.currentThread().isInterrupted().

That "cleared" detail causes a classic bug. This worker never stops when a shutdown interrupts it:

while (!Thread.currentThread().isInterrupted()) {
    try {
        pollQueue();
        Thread.sleep(50);
    } catch (InterruptedException e) {
        // ignored: "we'll check the flag at the top of the loop"
    }
}

The interrupt arrives during sleep, sleep throws and clears the flag, the catch block ignores it, and the loop's check finds the flag false. The signal is lost. There are two correct ways to handle InterruptedException:

// 1. Don't catch it: declare `throws InterruptedException` and let the caller decide.

// 2. Catch it, put the flag back, and stop:
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();   // restore the flag, so callers and loops can see it
    return;
}

Thread pools rely on this: shutting down a pool (shutdownNow()) or cancelling a task interrupts the threads running them. Swallowing the interrupt breaks both.

One more detail: the static Thread.interrupted() reads the flag and clears it, while isInterrupted() only reads it.

When a thread throws, and daemon threads

If an exception escapes a thread's run() method, only that thread dies. Java prints Exception in thread "worker" ... with the stack trace, and every other thread, main included, keeps going. So a service can lose a worker and carry on at reduced capacity without anyone noticing. Long-running worker loops should catch and log exceptions for each item they process, so one bad item doesn't kill the worker.

A program keeps running as long as any non-daemon thread is alive. A daemon thread (marked with setDaemon(true) before start()) is for background work that shouldn't keep the program alive. The catch: when the last non-daemon thread ends, daemon threads are simply abandoned, and their finally blocks don't run. Never give a daemon thread work that must finish, like writing a file.

ThreadLocal

A ThreadLocal holds a separate value for each thread:

private static final ThreadLocal<String> currentUser = new ThreadLocal<>();

currentUser.set("alice");    // only this thread sees "alice"
currentUser.get();           // "alice" here; null on every other thread

Frameworks use it to carry per-request information, such as the logged-in user, without passing it through every method. The trap is thread pools. A pool reuses the same thread for request after request, so a value one request set is still there for the next request on that thread, unless you remove it:

currentUser.set(user);
try {
    handle(request);
} finally {
    currentUser.remove();    // always clean up
}

Common mistakes

MistakeWhat happensDo this instead
Calling run() instead of start()the task runs on your own threadstart()
Starting a thread twiceIllegalThreadStateExceptiona new Thread, or a pool
Catching InterruptedException and carrying onshutdown and cancel stop workingrestore the flag and stop, or rethrow
No try/catch per item in a worker loopone bad item kills the workercatch and log each item's failure
Important work on a daemon threadabandoned at exit, finally skippeda normal thread, and a clean shutdown
ThreadLocal on a pool without remove()one request sees another's dataremove() in finally

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. What's the difference between t.run() and t.start()?

Show answer

start() creates a new thread that runs the task. run() is an ordinary method call: the task runs on your current thread, and nothing happens in parallel.

2. A worker catches InterruptedException from sleep, ignores it, and loops while !isInterrupted(). Why does it never stop?

Show answer

sleep clears the interrupt flag when it throws, so the loop's check sees false. Restore it with Thread.currentThread().interrupt() in the catch block (and stop), or let the exception propagate.

3. A worker thread throws an exception nobody catches. What happens to the rest of the program?

Show answer

Only that thread dies. The exception and stack trace are printed to standard error, and main and every other thread keep running.

4. Why can a ThreadLocal set during one request show up in a different request?

Show answer

Both requests ran on the same pool thread, and nothing removed the value after the first one. Call remove() in a finally block around the work.

Key takeaways

  • A thread is a separate path of execution sharing the same memory. start() creates one; run() doesn't.
  • join() waits for a thread; the six states are what you'll read in a thread dump.
  • Interruption is a polite request. Blocking calls throw and clear the flag, so restore it and stop, or rethrow.
  • An uncaught exception kills one thread only. Daemon threads die at exit without cleanup.
  • On a pool, always remove() a ThreadLocal in finally.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

The basics

Thread t = new Thread(task, "worker");   // task is a Runnable
t.start();                               // new thread runs task.run()  (t.run() would NOT)
t.join();                                // wait; afterwards you see everything t wrote
t.setDaemon(true);                       // before start(): doesn't keep the JVM alive
Thread.sleep(50);                        // pauses; keeps any locks held

A thread starts once; a second start() throws IllegalThreadStateException.

States

StateTypical cause
NEWnot started
RUNNABLErunning, ready, or blocked in native I/O
BLOCKEDwaiting to enter synchronized
WAITINGjoin(), wait(), park()
TIMED_WAITINGsleep(ms), join(ms), wait(ms)
TERMINATEDfinished

Interruption

t.interrupt();                                     // a request: sets the flag
Thread.currentThread().isInterrupted();            // read the flag
Thread.interrupted();                              // read AND clear it (static)

} catch (InterruptedException e) {                 // sleep/wait/join/take threw and CLEARED the flag
    Thread.currentThread().interrupt();            // restore it
    return;                                        // and stop (or rethrow instead of catching)
}

Exceptions, daemons, ThreadLocal

  • An uncaught exception kills that thread only; it's printed, everyone else carries on. Catch per item in worker loops.
  • The JVM exits when the last non-daemon thread ends; daemon threads are abandoned without finally.
  • ThreadLocal on a pool: set ... try { ... } finally { remove(); }.

The traps

  • run() instead of start().
  • An empty catch (InterruptedException e) {}: shutdown and cancel(true) stop working.
  • Worker loops without a per-item try/catch.
  • Work that must finish on a daemon thread.
  • ThreadLocal values leaking from one request to the next.

Staff-level points, one line each

  • Platform thread = one OS thread with its own stack: fine for hundreds, costly for tens of thousands. Hence pools, and virtual threads in Java 21.
  • Priorities are hints (ignored by default on Linux).
  • Thread.stop throws UnsupportedOperationException on JDK 21: cancellation is always cooperative.
  • Not interruptible: synchronized entry, Lock.lock(), classic socket reads (close the socket). Interruptible: lockInterruptibly, NIO channels.
  • LockSupport.park() returns on interrupt with no exception and the flag still set; spurious wakeups mean re-check in a loop.
  • Uncaught handler order: the thread's own, then the group/default handler, then stderr. submit keeps exceptions in the Future; execute sends them to the handler.
  • ThreadLocalMap lives in each Thread; weak keys, strong values: leaks on long-lived threads. ScopedValue is the newer alternative.
  • Shutdown: hooks run concurrently in no order; daemons halt mid-flight; SIGKILL skips everything; Kubernetes gives 30 s by default.
  • Thread dumps: BLOCKED clusters on one lock address, "Found one Java-level deadlock", compare several dumps over time.

Say it in an interview

  • "I'd restore the interrupt flag in the catch block, so shutdown still works."
  • "A socket read ignores interrupts, so to cancel it I'd close the socket."
  • "I'd set a default uncaught-exception handler that logs and counts, so a dead worker is visible."

Practice

This skill comes with 12 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java: Threads and Their Lifecycle free

A Worker That Stops on Time

MediumProCoding problemJava

A billing service in Seattle runs a background worker that takes jobs from a queue and processes them one at a time. During deploys, the service shuts down by interrupting the worker, and the platform kills anything still running after a few seconds.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Java: Threads and Their Lifecycle free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime