14.3
Asking a Thread to Stop
There is no method that stops a thread, and the one that used to has been made to throw. What replaced it is a request the thread has to agree to, and the way it agrees is easy to get wrong by accident.
Previously on
Section 14.2 ended on this:
Thread.stop()exists and has been deprecated for twenty years. Calling it on Java 21 throwsUnsupportedOperationException.
So a thread cannot be stopped from outside. This section is about what you do instead, and it is the last section where a thread is alone with itself.
The problem
You start a worker that polls a queue forever. The user clicks cancel. Stop the worker.
Thread worker = new Thread(() -> {
while (true) {
processNextJob();
}
});
worker.start();
// user clicks cancel
worker.???There is nothing to put there. No stop, no kill, no cancel.
The method that used to do this was taken away on purpose. Thread.stop() threw an error inside the target thread wherever it happened to be, which could be here:
synchronized (account) {
account.balance -= amount;
// <- stopped here
account.history.add(new Transfer(amount));
}The balance is reduced and the transfer was never recorded. Worse, stopping released the lock on the way out. The next thread to look at that account found a half updated object and no sign that anything was wrong.
There is no safe place to kill a thread from outside, because only the thread knows which of its own moments are safe.
So the answer has to be a request. And a request raises a harder question: how does a thread that is asleep hear it?
while (true) {
Job job = queue.take(); // blocks for hours if the queue is empty
process(job);
}A flag is no use to a thread that is not running. It could be inside sleep, or waiting on a queue, and it will not reach the line that checks the flag until something wakes it.
The idea
interrupt() sets a flag, and wakes anything that is waiting.
worker.interrupt();Two separate things happen, and which one you get depends on what the thread was doing.
| What the thread was doing | What interrupt() does to it | |
|---|---|---|
| Running normally | a loop, a calculation | sets the flag. Nothing else. The thread keeps going |
| Inside sleep, wait or join | not running | that method throws InterruptedException at once, and CLEARS the flag |
So a running thread has to look:
while (!Thread.currentThread().isInterrupted()) {
processNextJob();
}Verified: a busy loop that was interrupted ran another 132 million iterations before reaching the check. The flag was set the whole time and nothing looked at it.
And a waiting thread has to handle the exception:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// somebody asked us to stop
return;
}Two correct responses, and nothing else is correct.
Stop, if this method is allowed to decide:
catch (InterruptedException e) {
cleanUp();
return;
}Or put the flag back, if it is not your decision to make:
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore it
throw new RuntimeException("interrupted while loading", e);
}That second one matters in library code. You were interrupted, you cannot decide what should happen, so you make sure the caller can still find out.
The rest of the methods, briefly.
sleep(ms) pauses this thread for at least that long. It is a static method, so it always affects the current thread, and someOtherThread.sleep(100) puts you to sleep. It does not release any lock it is holding, which is a difference from wait() in Section 14.6 that causes real bugs.
join() waits for another thread to finish. join(500) waits at most half a second.
isAlive() says whether a thread has started and not yet finished.
yield() suggests to the scheduler that other threads could have a turn. It is a hint, the scheduler is free to ignore it, and it does nothing you can depend on.
setPriority(1..10) is passed to the operating system, which may ignore it entirely. On some platforms it has no effect at all.
Under the hood
Going deeperWhy interruption cannot be forced. The flag lives on the Thread object, and setting it is a plain write. There is no mechanism for making another thread jump somewhere, and there should not be, because that is exactly what Thread.stop() did.
The blocking methods are different because they are already inside the JVM. Thread.sleep is a native call that parks the thread with the operating system, and interrupting one of those threads wakes it and hands it the exception. That is why an interrupt reaches a sleeping thread instantly and a busy thread not at all.
Which blocking calls respond, and which do not.
| Responds to interrupt | Ignores it completely | |
|---|---|---|
| Thread.sleep, Object.wait, Thread.join | throws InterruptedException | |
| BlockingQueue.take, Lock.lockInterruptibly | throws InterruptedException | |
| synchronized (obj) | waits for the lock whatever happens | |
| InputStream.read on a socket | usually not. Close the socket instead |
That last row is a real operational problem. A thread stuck reading from a dead connection cannot be interrupted, and the usual answer is to close the socket from another thread so the read fails.
synchronized is the same story. A thread waiting to enter a synchronized block is BLOCKED and cannot be interrupted out of it. ReentrantLock in Section 14.7 has lockInterruptibly precisely because of this gap.
The shutdown pattern that actually works:
class Worker implements Runnable {
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Job job = queue.take(); // throws if interrupted while waiting
process(job);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore, for anything above
} finally {
cleanUp(); // runs either way
}
}
}Both halves are covered. The while condition catches an interrupt that arrives during process. The catch catches one that arrives during take. And finally from Section 12.1 makes sure cleanup happens on both paths.
What it costs
Interruption only works if every layer co-operates, and one uncooperative layer breaks it for everyone. A library that swallows InterruptedException makes your cancellation silently stop working, and there is nothing you can do from outside except stop using it.
It is also easy to get wrong in a way that looks right. An empty catch block compiles, runs, and appears to handle the exception. The request has been thrown away and the code looks tidier than the correct version.
Some waiting cannot be interrupted at all. A blocked socket read and a thread waiting on synchronized both ignore the flag entirely. So “cancel this” has no answer for large parts of a real program without a different design.
There is a timing hole as well. Interrupting a thread that has not started yet does nothing, and neither does interrupting one that has already finished. Cancelling something that has not begun is not possible with this mechanism.
And yield and priorities are close to useless. They look like control and they are hints that platforms may ignore, so anything whose correctness rests on them is not correct, it is lucky.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
What does `interrupt()` actually do?
Show the answer
It sets a flag on the thread. That is all it does.
It does not stop anything. A thread in a busy loop keeps running, and only notices if it chooses to look at the flag. Verified: an interrupted busy loop ran another 132 million iterations before it reached the line that checks.
The one special case is a thread that is waiting. If it is inside
sleep,waitorjoin, that method throwsInterruptedExceptionstraight away, which is how a sleeping thread finds out quickly.So interruption is a request, not a command. The thread has to co-operate, and code that never checks the flag can never be interrupted.
Catching `InterruptedException` and carrying on quietly is a bug. Why?
Show the answer
Because throwing it clears the flag. Verified: inside the catch block,
isInterrupted()returnsfalse.So the only record that anyone asked you to stop is the exception you are holding. Swallow it and the request has been erased. Nothing further up can find out, and nothing will ask again.
A loop that catches and continues ignores interruption completely. Verified: a thread that swallowed the exception went on to finish all three of its sleeps as if nothing had happened.
Two correct responses. Stop what you are doing, or call
Thread.currentThread().interrupt()to put the flag back so the next layer up can see it.Why was `Thread.stop()` taken away, when killing a thread sounds so useful?
Show the answer
Because it stopped the thread wherever it happened to be, including halfway through updating something.
A thread killed between two writes leaves an object in a state no code was written to handle. Worse, it released every lock it held on the way out, so other threads walked straight into that half finished object believing it was fine.
There is no safe moment to kill a thread from outside, because only the thread knows which of its moments are safe. That is exactly why the replacement asks rather than tells.
The method is still in the API on Java 21 and always throws
UnsupportedOperationException.
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 95 minutes
The Loop That Never Notices
ex-14-3-aAsk a thread to stop and watch it ignore you.
The busy loop is the honest case. Nothing is broken, nothing is slow, and the flag has been set for the entire time. The thread never looked, because you never told it to.
Then compare with the sleeping thread. Same call, same flag, and it reacts instantly. Work out why those two are so different before you read the hint, because the answer is the reason interruption works at all.
What your program must do
- Interrupt a busy loop that never checks, and show it keeps running
- Add the check and count how many iterations happened after the flag was set
- Interrupt a sleeping thread and compare how quickly it responds
- Explain why the two behave so differently
public class NeverNotices {
public static void main(String[] args) throws Exception {
// TODO: a thread in a busy loop that COUNTS but never checks the flag.
// Interrupt it. Does it stop? Give it two seconds, then give up.
// TODO: add a check of isInterrupted() to the loop condition. Now interrupt it.
// Print how many iterations it managed after the flag was set.
// TODO: a thread that only sleeps. Interrupt that. How fast does it react?
// TODO: say why the two react so differently
}
}
Hint 1
interrupt() sets a flag and nothing else. A thread that never reads it carries on exactly as before.Hint 2
Hint 3almost the answer
sleep is inside the JVM. Interrupting one wakes it and throws InterruptedException at once, which is the only way a waiting thread can hear you.The Empty Catch That Erased a Request
ex-14-3-bWrite the sloppy version first and count the sleeps.
All three finish. The interrupt arrived during the first one, the exception was caught, and the thread carried on as though nothing had been asked of it. That is not the thread being stubborn: the request was deleted by your catch block.
The print inside the catch is the part that explains it, and it is worth predicting before running. The value surprises nearly everyone, and once you have seen it the two correct fixes become obvious.
What your program must do
- Write the swallowing version and count how many sleeps complete
- Print the interrupt flag inside the catch and explain the value
- Fix it by returning, and again by restoring the flag
- Show that the restoring version lets a caller detect the interruption
public class Erased {
public static void main(String[] args) throws Exception {
// TODO: a thread that sleeps three times in a row, catching
// InterruptedException and doing NOTHING with it.
// Interrupt it during the first sleep. How many sleeps finish?
// TODO: inside the catch, print isInterrupted(). Predict it first.
// TODO: fix it two ways: return from the catch, and restore the flag
// TODO: for the restore version, show that a caller can still find out
}
}
Hint 1
Hint 2
Hint 3almost the answer
Thread.currentThread().interrupt() inside the catch. Use that when this method is not the right place to decide what should happen.The Shutdown That Actually Works
ex-14-3-cA worker loop has two places it can be when you ask it to stop, and covering one is a bug that shows up half the time.
Write it so both are handled, then test both on purpose. Interrupt while it is chewing on a job, and interrupt while it is sitting on an empty queue. If only one of your tests stops the thread, you have found the half you missed.
The cleanup belongs in a finally. There is no arrangement of catches that covers every exit as reliably, and you already know why from Phase XII.
What your program must do
- Write a worker that stops correctly whether it is waiting or working
- Interrupt it mid-job and confirm it stops and cleans up
- Interrupt it while waiting on an empty queue and confirm the same
- Say which part of your code handles which case
import java.util.concurrent.*;
public class Shutdown {
static final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
public static void main(String[] args) throws Exception {
// TODO: a worker that takes jobs from the queue and processes each one slowly.
// Handle an interrupt that arrives while WAITING on take().
// Handle an interrupt that arrives while PROCESSING.
// Clean up in both cases.
// TODO: feed it some jobs, then interrupt it mid-job. Does it stop?
// TODO: interrupt it while the queue is empty. Does it stop?
// TODO: confirm the cleanup ran on both paths
}
}
Hint 1
while (!Thread.currentThread().isInterrupted()) condition catches an interrupt that arrived during processing. The catch around take() catches one that arrived while waiting.Hint 2
BlockingQueue.take() throws InterruptedException, so it is one of the calls that responds to an interrupt straight away.Hint 3almost the answer
finally, from Section 12.1. Both exit paths go through it, and there is no third path to forget about.Two Methods, One Letter Apart
ex-14-3-dTwo methods, one letter, and one of them changes the thing it is reporting on.
Predict every answer before running. The pair that catches people is calling the clearing version twice: the first call says true and the second says false, and nothing in between touched the flag except the question.
Then write the loop with the wrong one deliberately. It reads correctly, it compiles, and it never stops. That is a bug you want to have met once in a place where it cost you nothing.
What your program must do
- Predict the results of calling each method twice, then check
- Write a loop that uses the clearing version by mistake and describe the result
- Say which of the two is static and why that matters
- Say when clearing the flag deliberately is the right thing to do
public class OneLetter {
public static void main(String[] args) throws Exception {
// TODO: interrupt the current thread, then call isInterrupted() twice.
// Predict both answers.
// TODO: interrupt it again, then call Thread.interrupted() twice.
// Predict both answers.
// TODO: write a worker loop that uses Thread.interrupted() as its condition
// BY MISTAKE. What happens when you interrupt it?
// TODO: say when you would deliberately want the clearing version
}
}
Hint 1
isInterrupted() leaves the flag alone. Thread.interrupted() returns the value and sets it back to false.Hint 2
Thread.interrupted() is static, so it always asks about the current thread. Writing someOtherThread.interrupted() compiles and asks about the wrong thread entirely.Hint 3almost the answer
After the credits
Every thread so far has kept to itself. Locals on its own stack, its own flag, its own sleep.
Now two of them touch the same field:
static int counter = 0;
// two threads, 100,000 increments eachYou expect 200,000. You will not get it, you will get a different wrong number every run, and nothing will throw.
Section 14.4 takes that apart. counter++ is not one instruction, it is three, and the failure is what happens when two threads interleave those three. It also covers two problems that are less obvious and worse: a thread reading a value that another thread updated minutes ago and never seeing the change, and the compiler reordering your lines because it is allowed to.
You have already seen this failure. Section 11.6 turned 100,000 into 40,787 with a parallel stream, and this is the explanation.
Threads you opened in this section
- InterruptionA thread waiting on synchronized cannot be interrupted at all, which is a real gap.14.5 - The Lock Every Object Already Has
Interruption will return in 14.10 - Stop Making Threads