Why this matters
- The
start()versusrun()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
// 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();
| Aspect | extends Thread | implements Runnable |
|---|---|---|
| Inheritance | Uses the single extends slot | Leaves it free |
| Separation | Task and thread are one object | The task is independent of how it runs |
| Reusable | A Thread cannot be restarted | The same Runnable can be submitted many times |
| Works with executors | Awkwardly | Directly |
| Recommended | No | Yes |
Inheritance
extends ThreadUses the single extends slotimplements RunnableLeaves it freeSeparation
extends ThreadTask and thread are one objectimplements RunnableThe task is independent of how it runsReusable
extends ThreadA Thread cannot be restartedimplements RunnableThe same Runnable can be submitted many timesWorks with executors
extends ThreadAwkwardlyimplements RunnableDirectlyRecommended
extends ThreadNoimplements RunnableYes
Prefer Runnable. In practice, prefer an executor over creating threads at all.
The six states
- 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.
Thread.State state = thread.getState();
These names are exactly what appears in a thread dump, which is why they are worth memorising:
Essential methods
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.
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 insleep,waitorjoinwakes withInterruptedException.- Catching
InterruptedExceptionclears 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
InterruptedExceptionis 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.
Thread background = new Thread(() -> {
while (true) {
collectMetrics();
sleepQuietly(60_000);
}
});
background.setDaemon(true); // must be set before start()
background.start();
| Aspect | User thread | Daemon thread |
|---|---|---|
| Keeps the JVM alive | Yes | No |
| On JVM exit | The JVM waits for it | Killed abruptly, no finally blocks |
| Default | Yes — inherits from the creating thread | Must be set explicitly |
| Suitable for | Work that must complete | Background monitoring, metrics, housekeeping |
Keeps the JVM alive
User threadYesDaemon threadNoOn JVM exit
User threadThe JVM waits for itDaemon threadKilled abruptly, no finally blocksDefault
User threadYes — inherits from the creating threadDaemon threadMust be set explicitlySuitable for
User threadWork that must completeDaemon 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.
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.
// 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. Onlystart()creates one. - "A thread can be restarted."
start()on a terminated thread throws. Create a new one. - "
sleep()releases locks." It does not. Onlywait()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 extendingThread, 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; onlywait()releases them.- Interruption is cooperative: catching
InterruptedExceptionclears the flag, so rethrow or callThread.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.