14.2
Two Ways to Make One, and Six States
Extending Thread and implementing Runnable both work, and one of them spends your only superclass. Then six states, three of which mean a thread is not running for three different reasons.
Previously on
Section 14.1 made a thread with a lambda, and left the older way unexplained:
class Worker extends Thread {
public void run() { ... }
}Both work. One of them costs you something you cannot get back.
The problem
You need a class that does some work on its own thread. There are two ways to write it and no obvious reason to prefer either.
class ReportJob extends Thread {
public void run() { buildReport(); }
}
new ReportJob().start();class ReportJob implements Runnable {
public void run() { buildReport(); }
}
new Thread(new ReportJob()).start();The first is shorter. Most tutorials show it first. It is nearly always the wrong one, and the reason has nothing to do with threads.
Now the second problem, which is worse. Start a thread and something goes wrong inside it:
Thread worker = new Thread(() -> {
throw new RuntimeException("could not reach the database");
});
worker.start();
System.out.println("carrying on");carrying on prints. The program does not stop. A stack trace appears on the console and nothing else happens, and if nobody is watching the console, nothing happened at all.
You cannot catch it either:
try {
worker.start();
} catch (RuntimeException e) {
// never runs. start() returned long before the failure.
}start() returned immediately. By the time the worker failed, this thread was somewhere else entirely, and there is no stack to unwind into.
And the third problem is that “running” is not one thing. A program that has stopped responding might have every thread alive and none of them working. Some are waiting for a lock. Some are waiting for a signal. Some are asleep. Those are three different bugs, and “the program is stuck” does not tell you which.
The idea
Implement Runnable. Almost always.
| extends Thread | implements Runnable | |
|---|---|---|
| Your one superclass | spent | still free |
| The relationship | says your job IS a thread | says your job is work to run |
| Reuse | one thread, once | a thread, an executor, a scheduled task |
| As a lambda | not possible | yes. One abstract method |
Java has single inheritance, from Section 7.1. Extending Thread spends it, and a class that extends Thread can never extend anything else for the rest of its life.
It is also the wrong shape. extends means is a. A report job is not a kind of thread. It is work, and a thread is the thing that runs work.
The practical reason is the one that decides it. A Runnable can be given to a Thread today and to an executor in Section 14.10 without changing a line. A subclass of Thread is tied to being a thread, and a thread can only be started once.
A thread moves through six states, and you can watch it.
| State | What it means | |
|---|---|---|
| NEW | made, not started | no operating system thread exists yet |
| RUNNABLE | ready or running | Java does not separate the two. The scheduler decides |
| BLOCKED | wants a lock somebody holds | will run when the lock is free. Nobody has to be told |
| WAITING | waiting with no deadline | someone must call notify, or finish, or it waits forever |
| TIMED_WAITING | waiting with a deadline | wakes on its own when the time is up |
| TERMINATED | finished | cannot be restarted |
Verified by asking a running program:
before start() : NEW
busy loop : RUNNABLE
Thread.sleep : TIMED_WAITING
lock.wait() : WAITING
waiting for a lock : BLOCKED
after it finished : TERMINATEDThe three not-running states are three different problems.
BLOCKED means contention. Several threads want the same lock, and they are taking turns. It resolves on its own.
WAITING means a thread is expecting something. If the signal never comes, it waits forever, and a group of threads in WAITING all expecting each other is a deadlock.
TIMED_WAITING means it will come back by itself.
And you can be told when one dies:
worker.setUncaughtExceptionHandler((t, e) ->
log.error("thread {} died: {}", t.getName(), e.getMessage(), e));handler saw: boom / thread died
main is still alive : trueSet it before start(). Without one, the default prints the trace to the console and that is the whole notification you get.
Under the hood
Going deeperWhy an exception cannot travel to the starter. Each thread has its own stack, from Section 13.1. Unwinding means walking down a stack looking for a catch, and the worker’s stack has only the worker’s frames on it.
There is no frame belonging to main underneath. main called start() and moved on, possibly finishing entirely. There is nowhere to unwind to, so the thread runs out of stack, the default handler prints, and the thread ends.
This is the thing that makes threads hard to reason about. Every other failure in this course travelled up a chain of callers. Here the chain has one link.
Runnable cannot throw a checked exception:
public interface Runnable {
void run(); // no throws clause
}So a checked exception has to be caught inside the lambda and wrapped, which is exactly the clash from Section 12.2. It is not an oversight. Adding throws would mean every caller of run() had to handle it, and the caller is the JVM.
Callable in Section 14.10 is the answer. It returns a value and it can throw, and both of those are only possible because somebody is waiting for the result.
join() is how one thread waits for another:
worker.start();
worker.join(); // this thread goes WAITING until worker is TERMINATEDSkipping it is a common bug. Without join, the next line runs while the worker is still going, and whatever the worker was producing is not ready.
Naming threads is not cosmetic.
new Thread(job, "report-worker-1").start();Default names are Thread-0, Thread-1, and so on, in creation order. In a thread dump from a production incident those tell you nothing. A real name is often the difference between finding the stuck thread in a minute and in an hour.
Daemon status is inherited and must be set before starting. A thread created by a daemon thread is a daemon. setDaemon after start() throws IllegalThreadStateException, which is the same exception you get for starting twice, and for the same reason: the thread has already left the state where that made sense.
What it costs
The biggest cost here is silence. A worker that dies takes its failure with it, and your program keeps running while doing less than you think. Nothing surfaces unless you set a handler or watch a console, and by then the symptom is usually somewhere else.
The states help less than they look like they will. getState is a snapshot that may already be wrong by the time you read it, and it is documented as being for monitoring rather than for control. Code that branches on a thread’s state is code with a race in it.
join() has its own trap. A thread waiting in join is WAITING, and if the thread it is waiting for is itself stuck, both are now stuck and neither will ever report anything. Every join is a promise that the other thread finishes.
Naming and handlers both have to be done before start(), and forgetting is easy because nothing complains until you need them, which is during an incident.
And extending Thread will keep appearing. It is in every old codebase and half the tutorials, and it looks tidier than the alternative. The cost of it is invisible right up until the day you need that class to extend something else.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
Why is implementing `Runnable` almost always better than extending `Thread`?
Show the answer
Because extending spends the one superclass you have. Java allows single inheritance only, from Section 7.1, so a class that extends
Threadcan never extend anything else.It is also the wrong relationship. Your class is a piece of work, not a kind of thread. Extending says is a, and a payroll job is not a kind of thread.
The practical reason is reuse. A
Runnablecan be handed to aThread, to an executor, to a scheduled task. A subclass ofThreadcan only be one thread, once, because a thread that has finished cannot be started again.Three of the six states mean the thread is not running. What is different about them?
Show the answer
BLOCKED means it is waiting to get into a
synchronizedblock. It wants a lock somebody else is holding. It will run the moment that lock is released, and nobody has to tell it anything.WAITING means it called something with no time limit, like
wait()orjoin(). It will not wake up on its own. Another thread has to callnotifyor finish.TIMED_WAITING is the same with a deadline, like
Thread.sleep(1000). It wakes on its own when the time is up.The difference matters when a program has stopped. Threads sitting in BLOCKED are fighting over a lock. Threads in WAITING are expecting a signal that may never come, and that is what a deadlock looks like in a thread dump.
A thread throws an exception nobody catches. What happens to the program?
Show the answer
That thread dies. Its stack trace is printed. Everything else carries on as if nothing happened.
The exception does not travel to whoever called
start(), because that thread has long since moved on. Each thread has its own stack, so there is nowhere for it to unwind to.That makes a failed worker invisible. Your program keeps running with one fewer thread doing its job, and unless you are reading the console nothing tells you.
setUncaughtExceptionHandleris how you find out. Verified: the handler is called with the thread and the exception, andmaincontinues normally.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises95 pointsabout 100 minutes
Spend Your Only Superclass
ex-14-2-aThe advice is everywhere. Feel the cost yourself and it stops being advice.
Write the version that extends Thread first, and then try to also extend your own base class. The error you get is the whole argument, and it arrives long after the decision was made.
The reuse part is the practical half. One Runnable handed to two threads is ordinary. The same trick with a Thread subclass is not possible at all, and that limitation is exactly what makes the executors in Section 14.10 necessary.
What your program must do
- Try to extend both Thread and your own base class, and record the error
- Write the Runnable version extending the base class successfully
- Hand one Runnable to two different Threads, and try the same with a Thread subclass
- Write the Runnable as a lambda and say why the other version cannot be one
public class Spent {
static abstract class Job {
abstract String name();
void report() { System.out.println("job: " + name()); }
}
// TODO: write ReportJob extending Thread. Now also make it extend Job.
// Read the error.
// TODO: write the same thing implementing Runnable, and extending Job as well
public static void main(String[] args) throws Exception {
// TODO: run both versions
// TODO: hand the Runnable to a Thread, then hand the SAME object to a
// second Thread. Try the equivalent with your Thread subclass.
// TODO: write the Runnable version as a lambda. Try that with Thread.
}
}
Hint 1
Thread uses it up, and no error appears until the day you need the class to be something else as well.Hint 2
Runnable is a piece of work, so the same object can be given to as many threads as you like. A Thread is a thread, and a thread that has finished cannot be restarted.Hint 3almost the answer
Runnable has one abstract method, so it is a functional interface and a lambda works. Thread is a class with many methods, so there is nothing for a lambda to be.Catch a Thread in Every State
ex-14-2-bGet a real thread into each state and read it back.
Six states, six small experiments, and the fiddly part is timing: start() returns before the thread has reached the interesting line, so the observer has to wait a moment before looking.
The last question is the one that pays off later. When a service stops responding, the first thing you do is take a thread dump, and it is a list of these states. Threads queueing on a lock and threads waiting for a signal that never comes look similar and mean completely different things.
What your program must do
- Observe all six states from a running program
- Explain why the observer has to pause before reading each state
- Say what each of the three not-running states means when a program is stuck
- Say why Java has no separate state for actually executing right now
public class SixStates {
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
// TODO: get a thread into each of the six states and print getState()
// NEW easy
// RUNNABLE a busy loop
// TIMED_WAITING Thread.sleep
// WAITING lock.wait(), or join on a long thread
// BLOCKED one thread holds a lock while another wants it
// TERMINATED let one finish
// TODO: make the observing thread sleep briefly before each check. Why?
// TODO: say what each of BLOCKED, WAITING and TIMED_WAITING means for a
// program that has stopped responding
}
}
Hint 1
Hint 2
start() returns before the new thread has reached the line you care about.Hint 3almost the answer
A Worker Dies Quietly
ex-14-2-cStart a worker that fails, and try to catch it the way you would catch anything else.
The try around start() never fires. Not because the exception was handled somewhere, and not because it did not happen. There was no path at all from the failure back to you.
The five worker version is the one that should worry you. Four threads carry on, the program looks healthy, and a fifth of your work is no longer being done. Work out how you would have noticed, and then set the handler.
What your program must do
- Wrap start() in a try and show the catch never runs
- Confirm the program continues after a worker dies
- Add an uncaught exception handler and see the failure reported
- Explain why the exception cannot travel to the thread that called start
public class Quiet {
public static void main(String[] args) throws Exception {
// TODO: start a thread that throws. Wrap start() in a try/catch.
// Predict whether the catch runs.
// TODO: confirm main keeps going afterwards
// TODO: add setUncaughtExceptionHandler and try again
// TODO: start five workers, have one of them throw, and show the program
// carries on with four. How would you have found out without the handler?
// TODO: explain why the exception cannot reach main, using stacks
}
}
Hint 1
start() returns immediately. By the time the worker fails, main is somewhere else entirely, and there is no frame of main's underneath the worker to unwind into.Hint 2
Hint 3almost the answer
start(). Without one you get the default, which prints to the console, and in a service nobody is reading the console.Join, or Read Nothing
ex-14-2-dRun the unjoined version five times and write down all five answers.
Some runs will be short. At least one will probably be complete, and that is the outcome to be suspicious of. Code that gives the right answer most of the time is code that will pass review, pass your tests, and fail in production.
Changing the sleep lengths makes the point sharper. Nothing about your logic changed, and the result did, which means the logic was never what decided it.
What your program must do
- Print the results without joining and run it five times
- Join all four and run it again, five times
- Vary the sleep lengths and describe how the unjoined results change
- Say what state the joining thread is in while it waits
import java.util.*;
public class Joining {
public static void main(String[] args) throws Exception {
List<String> results = Collections.synchronizedList(new ArrayList<>());
// TODO: start four threads, each adding one result after a short sleep
// TODO: print results WITHOUT joining. Run it five times.
// TODO: now join all four first, and run it five times again
// TODO: give the threads different sleep lengths. Does the missing count change?
// TODO: what state is main in while it is inside join()?
}
}
Hint 1
main reaches the print while the workers are still going. How many made it in depends entirely on timing, so the answer changes between runs.Hint 2
Hint 3almost the answer
join() is WAITING. It has no deadline and it will only continue when the other thread reaches TERMINATED.After the credits
Threads have to be told to stop, and there is no method for it.
Thread.stop() exists and has been deprecated for twenty years, because killing a thread mid instruction can leave a shared object half updated with a lock still held.
Since Java 20 the method is still there and no longer does anything. Calling it on Java 21 throws UnsupportedOperationException, which is a fairly rare way for a language to remove a feature: the code still compiles and fails the moment it runs.
Section 14.3 covers what replaced it: interrupt, which is a polite request the thread has to agree to, plus sleep, yield, join and the priority hint that mostly does nothing. It also shows what InterruptedException really means, which is not what its name suggests.
That section is the last one where a thread is alone with itself. From Section 14.4 onwards, two threads touch the same field, and the answer stops being right.
Thread states will return in Phase XIV. Concurrency