Interrupts and cancellation
Why Java can't kill a thread, what interrupt() really does to running, sleeping, parked and I/O-blocked threads, the two correct ways to handle InterruptedException, and how executors cancel and shut down on top of it.
On this page
- Picture it first: a tap on the shoulder, not a trapdoor
- Why you can't kill a thread: the Thread.stop() story
- The interrupt status: one flag, three methods
- What responds to an interrupt, and what ignores it
- Under the hood: interrupting a sleeping worker
- CPU-bound loops must poll
- Handling InterruptedException: two right answers, one bug
- The swallowed interrupt that breaks shutdown
- Cancelling tasks: futures, executors and timeouts
- Real world
- Advanced: edge cases, virtual threads and comparisons
- How to say it in the interview
✋ What are interrupts and cancellation?
Cancellation means asking a running task to stop before it finishes: a user closes the tab, a 2-second deadline passes, or a deploy shuts the service down. Java has no safe way to kill a thread from outside, so it gives you a polite request instead: t.interrupt() sets a boolean interrupt status on thread t. Blocking calls like Thread.sleep notice it and throw InterruptedException; everything else has to check the flag and decide to stop. Cancellation in Java is cooperative, always.
For this lesson, we'll build the whole model: why Thread.stop() had to die, what interrupt() actually does to a running, sleeping, parked or I/O-blocked thread, the only two correct ways to handle InterruptedException, and how executors, futures and shutdown hooks are built on top of it. Interviewers love this topic because almost everyone has written catch (InterruptedException e) {} at some point, and the follow-up "what does that break?" separates people who copy patterns from people who understand them.
Below the line (out of scope)
- Thread states such as
TIMED_WAITINGand how a thread gets there: Thread lifecycle and states - Why
wait()must be called in a loop: wait(), notify() and notifyAll() - Pool sizing and the
ThreadPoolExecutorinternals: the "ExecutorService and thread pools" lesson CompletableFuturecomposition: the "How CompletableFuture works" lesson
The 30-second answer
You can't safely kill a thread: Thread.stop() was deprecated in 1.2 because it unwound the victim's stack and released all its monitors mid-update, and since Java 20 it only throws UnsupportedOperationException. Instead you call t.interrupt(), which sets the thread's interrupt status. sleep, wait, join and the java.util.concurrent blocking calls respond by clearing the flag and throwing InterruptedException; running code must poll isInterrupted(). When you catch InterruptedException, either let it propagate or restore the flag with Thread.currentThread().interrupt() and exit. Never swallow it, because executors cancel tasks (Future.cancel(true), shutdownNow()) by interrupting worker threads.
What you’ll learn
- Explain why
Thread.stop()is unsafe (ThreadDeath releases every monitor mid-update) and its status: deprecated in 1.2, throwingUnsupportedOperationExceptionsince Java 20 - Describe interruption as a cooperative flag and contrast
isInterrupted()(read) with staticThread.interrupted()(read and clear) - Predict what
interrupt()does to a thread insleep/wait/join,LockSupport.park, anInterruptibleChannel, aSelector, asynchronizedentry and a CPU loop - Handle
InterruptedExceptionthe only two correct ways: propagate it, or restore the flag and exit, and explain why swallowing it breaks pool shutdown - Apply the interruption policy rule: only the thread's owner decides what an interrupt means
- Cancel work with
Future.cancel(true),shutdown()/shutdownNow()and theawaitTerminationrecipe, and know thatCompletableFuture.canceldoes not interrupt - Choose between a
volatileflag, interrupts, timeouts and a poison pill, and describe how virtual threads andStructuredTaskScopeuse interrupts
Interview radar
Asked very often: How do you stop a thread in Java? Why is Thread.stop() deprecated?
Core follow-up: What should you do when you catch InterruptedException? interrupted() vs isInterrupted()?
Production angle: Why does our service take 30 seconds to shut down and then get killed? (Spoiler: a swallowed interrupt.)
01Picture it first: a tap on the shoulder, not a trapdoor
Imagine a chef halfway through plating a dish. Thread.stop() was a trapdoor under the chef: they vanish mid-plate, the half-assembled dish goes out to a customer, and the oven they had locked is suddenly unlocked with no one watching it. An interrupt is a tap on the shoulder: "please wrap up". A chef who is waiting (for an oven timer, for an order) reacts at once. A chef in the middle of chopping finishes the cut, checks for taps between cuts, and stops at a clean point.
That tap lands differently depending on what the thread is doing at the moment. This is the single most important picture in the lesson:
One call to t.interrupt() sets a flag, and the outcome depends on whether t is running, sleeping, parked, doing channel I/O, selecting, or blocked on a monitor
Notice the asymmetry: sleep clears the flag when it throws, while park, channels and selectors leave it set. That one detail explains most interrupt bugs you'll ever debug.
02Why you can't kill a thread: the Thread.stop() story
Java 1.0 shipped Thread.stop(), suspend() and resume(). stop() made the victim throw a ThreadDeath error at whatever bytecode it was executing. As ThreadDeath unwound the stack, every synchronized block exited and every monitor the thread held was released. Picture transfer() holding a lock, having debited account A but not yet credited account B. The lock opens, other threads see money that vanished, and no exception tells them anything is wrong.
suspend() had the opposite problem: the thread froze while still holding its locks. If the thread meant to call resume() needed one of those locks first, the process deadlocked. Here's how the JDK has wound these methods down:
| Release | What changed |
|---|---|
| JDK 1.2 (1998) | stop(), suspend(), resume() deprecated, with Sun's "Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?" note (still in the Java 21 docs) |
| JDK 11 | Thread.stop(Throwable) and Thread.destroy() removed |
| JDK 14 | suspend()/resume() deprecated for removal |
| JDK 18 | stop() deprecated for removal (JDK-8277861) |
| JDK 20 | stop(), suspend(), resume() degraded to always throw UnsupportedOperationException (JDK-8289610, JDK-8249627); ThreadDeath deprecated for removal |
| Java 21 (this course) | All three still compile and always throw UnsupportedOperationException |
| JDK 23 / JDK 26 | suspend()/resume() and ThreadGroup.stop() removed in 23; Thread.stop() removed in 26 (JDK-8368226), so calls no longer compile |
The replacement the Javadoc has recommended since 1.2 is the cooperative pattern: the target thread checks a request regularly and returns from run() in an orderly way, and interrupt() is the way to wake it from long waits. Only the thread itself knows where its consistent stopping points are.
03The interrupt status: one flag, three methods
Every Thread carries one boolean. Three methods touch it:
| Method | Called on | Reads | Clears? | Typical use |
|---|---|---|---|---|
t.interrupt() |
any thread object | n/a, sets it to true | n/a | Ask t to stop |
t.isInterrupted() |
any thread object | yes | No | Loop condition: while (!Thread.currentThread().isInterrupted()) |
Thread.interrupted() |
static, always the current thread | yes | Yes, resets to false | "Consume" the request, e.g. before throwing InterruptedException yourself |
Here's the everyday version. The interrupt status is a sticky note on someone's monitor saying "please wrap up". isInterrupted() is glancing at the note and leaving it there. Thread.interrupted() is reading it, peeling it off and throwing it in the bin. Whoever looks next sees a clean monitor.
Many candidates mix the last two up because the names are so close. The static one has a side effect: call it twice in a row and the second call returns false. Use it only when you're about to act on the request (throw, or exit), otherwise you have destroyed information someone else needed.
04What responds to an interrupt, and what ignores it
Here's the full inventory I keep in my head. The middle column is what you get; the right column is what to do when a call ignores interrupts.
| Blocking operation | Reaction to interrupt() |
Flag afterwards |
|---|---|---|
Thread.sleep, Object.wait, Thread.join |
throws InterruptedException |
cleared |
BlockingQueue.take/put, Future.get, CountDownLatch.await, Semaphore.acquire, Lock.lockInterruptibly, Condition.await, awaitTermination |
throws InterruptedException |
cleared |
LockSupport.park / parkNanos |
returns silently, no exception | still set |
I/O on an InterruptibleChannel (SocketChannel, FileChannel) |
channel closed, ClosedByInterruptException |
still set |
Selector.select() |
returns early, like wakeup() |
still set |
Entering a synchronized block, Lock.lock() |
nothing, keeps waiting | set, seen later |
Classic java.io socket InputStream.read() on a platform thread |
nothing, keeps waiting | set, seen later |
| A CPU loop that never checks | nothing | set, seen later |
For the non-responders: replace synchronized with lock.lockInterruptibly() or tryLock(timeout); give sockets a setSoTimeout(...) so reads wake up periodically, or have the cancelling code close the socket, which makes the blocked read() throw SocketException; and make CPU loops poll the flag, which we'll do in a minute.
05Under the hood: interrupting a sleeping worker
Try it yourself first
Before reading on, predict three things. Inside the catch, is isInterrupted() true or false? What does Thread.interrupted() return the second time in a row? And does LockSupport.park() block forever if the flag is already set?
Here's the smallest program I use to show all of it. It runs on Java 21:
import java.util.concurrent.locks.LockSupport;
public class InterruptSleeper {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int polls = 0;
try {
while (true) {
polls++;
Thread.sleep(500); // blocking call: responds to interrupt
}
} catch (InterruptedException e) {
Thread me = Thread.currentThread();
System.out.println("worker: caught InterruptedException after " + polls + " polls");
System.out.println("worker: flag inside catch = " + me.isInterrupted());
me.interrupt(); // restore the flag for whoever owns this thread
System.out.println("worker: flag after restore = " + me.isInterrupted());
}
}, "worker");
worker.start();
Thread.sleep(1_200);
System.out.println("main: state before interrupt = " + worker.getState());
worker.interrupt();
worker.join();
System.out.println("main: worker is " + worker.getState());
// Same flag, read two different ways, on the main thread itself
Thread.currentThread().interrupt();
System.out.println("isInterrupted() = " + Thread.currentThread().isInterrupted());
System.out.println("isInterrupted() = " + Thread.currentThread().isInterrupted());
System.out.println("interrupted() = " + Thread.interrupted()); // reads AND clears
System.out.println("interrupted() = " + Thread.interrupted());
Thread.currentThread().interrupt();
long t0 = System.nanoTime();
LockSupport.park(); // returns at once: no exception, flag kept
System.out.printf("park returned in %d us, flag = %b%n",
(System.nanoTime() - t0) / 1_000, Thread.interrupted());
}
}
Real output from java InterruptSleeper.java on my laptop:
main: state before interrupt = TIMED_WAITING
worker: caught InterruptedException after 3 polls
worker: flag inside catch = false
worker: flag after restore = true
main: worker is TERMINATED
isInterrupted() = true
isInterrupted() = true
interrupted() = true
interrupted() = false
park returned in 57 us, flag = true
Notice four things:
- The sleeper reacted at once. It was in
TIMED_WAITINGinsidesleep(500); the interrupt cut that sleep short instead of waiting for it to end. - The flag was already false inside
catch.sleepcleared it on the way out. If this catch block did nothing, the request would be gone for good. isInterrupted()is a pure read;interrupted()is read-and-reset. Same flag, different side effects.park()returned in microseconds with the flag still set. It doesn't throw, so code built onparkmust check the flag itself. That's exactly whatBlockingQueue.take()and friends do internally.
06CPU-bound loops must poll
Try it yourself first
Two threads do 3 billion iterations of arithmetic. One never looks at the flag; the other checks it every 1,024 iterations. Both are interrupted 100 ms in. How long does each take to stop?
public class CpuLoops {
static final long WORK = 3_000_000_000L; // about 2 s of arithmetic on a laptop
public static void main(String[] args) throws InterruptedException {
race("deaf", () -> { // never looks at the flag
long acc = 0;
for (long i = 0; i < WORK; i++) acc += i ^ (acc >>> 3);
System.out.println(" deaf: finished all work, acc=" + acc);
});
race("polite", () -> { // checks the flag every 1,024 iterations
long acc = 0;
for (long i = 0; i < WORK; i++) {
if ((i & 1023) == 0 && Thread.currentThread().isInterrupted()) {
System.out.println(" polite: saw the flag at i=" + i + ", acc=" + acc);
return;
}
acc += i ^ (acc >>> 3);
}
});
}
static void race(String name, Runnable body) throws InterruptedException {
Thread t = new Thread(body, name);
t.start();
Thread.sleep(100);
long asked = System.nanoTime();
t.interrupt();
t.join();
System.out.printf("%s: stopped %d ms after interrupt(), flag still set = %b%n",
name, (System.nanoTime() - asked) / 1_000_000, t.isInterrupted());
}
}
deaf: finished all work, acc=1601616558616779095
deaf: stopped 2210 ms after interrupt(), flag still set = true
polite: saw the flag at i=127713280, acc=2318776813924687298
polite: stopped 1 ms after interrupt(), flag still set = true
The deaf loop ran for another 2.2 seconds and finished all its work as if nothing had happened; the flag sat there, set and unread. The polite loop stopped within about a millisecond. Your numbers will differ by machine, but the shape won't. (isInterrupted() after termination returning true is documented JDK behaviour since JDK 14; the spec itself only promises it "need not have any effect" on a dead thread.)
07Handling InterruptedException: two right answers, one bug
InterruptedException is checked, so the compiler forces you to decide. There are exactly two correct decisions, and the figure shows when to use each.
When you catch InterruptedException, propagate it if your method can declare it, otherwise restore the flag and exit; swallowing it is the bug
Here's what both look like in code:
// 1. Propagate: the method is blocking, so say so in the signature.
Job nextJob() throws InterruptedException {
return queue.take();
}
// 2. Restore and exit: run() can't throw checked exceptions.
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
process(queue.take());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // keep the request visible to the pool
} finally {
releaseResources();
}
}
Think of a receptionist who takes the call "fire drill, everyone out", says "noted" and hangs up without telling anyone. The building never empties. That's what swallowing an interrupt does to your pool.
The anti-pattern is catch (InterruptedException e) { } or log.warn("interrupted", e); followed by carrying on. Since sleep/take already cleared the flag, swallowing erases the only record of the request. Translating it into an unchecked exception without restoring the flag is the same bug in disguise.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- The swallowed interrupt that breaks shutdown
- Cancelling tasks: futures, executors and timeouts
- Real world
- Advanced: edge cases, virtual threads and comparisons
- How to say it in the interview
From ₹299 · one-time payment · No auto-renewal
Further reading (17)
- Thread (Java SE 21 API): interrupt(), interrupted(), isInterrupted(), stop(): the six interrupt cases; stop/suspend/resume always throw UnsupportedOperationException
- Java Thread Primitive Deprecation (Java SE 21 docs): why stop, suspend and resume were deprecated
- JDK-8289610: Degrade Thread.stop (JDK 20): related: JDK-8249627 (suspend/resume, JDK 20), JDK-8277861 (JDK 18), JDK-8320532 (removed in 23), JDK-8368226 (stop removed in 26)
- ExecutorService (Java SE 21 API): shutdown, shutdownNow, awaitTermination, the shutdownAndAwaitTermination recipe, close() since 19
- CompletableFuture.cancel (Java SE 21 API): mayInterruptIfRunning has no effect
- LockSupport (Java SE 21 API): park returns on interrupt without throwing
- InterruptibleChannel (Java SE 21 API)
- JEP 444: Virtual Threads: interrupts supported; java.net socket I/O interruptible in virtual threads
- JEP 453: Structured Concurrency (Preview, Java 21): shutdown() interrupts unfinished subtasks
- Brian Goetz: Java theory and practice: Dealing with InterruptedException (IBM developerWorks, 2006, archived)
- Java Concurrency in Practice (Goetz et al., 2006), chapter 7: Cancellation and Shutdown: interruption policies, task vs thread ownership
- KafkaConsumer Javadoc: Multi-threaded Processing and wakeup()
- KAFKA-4375: Kafka consumer may swallow some interrupts meant for the calling thread: and KAFKA-4387 (infinite loop on interrupted commitSync), both fixed in 0.10.2.0
- HBASE-10497: swallowed InterruptedException and interrupt() inside loops
- Guava Uninterruptibles: blocks through interrupts, then re-interrupts
- Spring ExecutorConfigurationSupport (ThreadPoolTaskExecutor shutdown): waitForTasksToCompleteOnShutdown, awaitTerminationSeconds
- Apache Tomcat 10.1 HTTP Connector: executorTerminationTimeoutMillis