Course contents

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.

PremiumMust know for interviewsLesson20 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a tap on the shoulder, not a trapdoor
  2. Why you can't kill a thread: the Thread.stop() story
  3. The interrupt status: one flag, three methods
  4. What responds to an interrupt, and what ignores it
  5. Under the hood: interrupting a sleeping worker
  6. CPU-bound loops must poll
  7. Handling InterruptedException: two right answers, one bug
  8. The swallowed interrupt that breaks shutdown
  9. Cancelling tasks: futures, executors and timeouts
  10. Real world
  11. Advanced: edge cases, virtual threads and comparisons
  12. 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_WAITING and 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 ThreadPoolExecutor internals: the "ExecutorService and thread pools" lesson
  • CompletableFuture composition: 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, throwing UnsupportedOperationException since Java 20
  • Describe interruption as a cooperative flag and contrast isInterrupted() (read) with static Thread.interrupted() (read and clear)
  • Predict what interrupt() does to a thread in sleep/wait/join, LockSupport.park, an InterruptibleChannel, a Selector, a synchronized entry and a CPU loop
  • Handle InterruptedException the 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 the awaitTermination recipe, and know that CompletableFuture.cancel does not interrupt
  • Choose between a volatile flag, interrupts, timeouts and a poison pill, and describe how virtual threads and StructuredTaskScope use 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:

Fig. 1

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

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:

  1. The sleeper reacted at once. It was in TIMED_WAITING inside sleep(500); the interrupt cut that sleep short instead of waiting for it to end.
  2. The flag was already false inside catch. sleep cleared it on the way out. If this catch block did nothing, the request would be gone for good.
  3. isInterrupted() is a pure read; interrupted() is read-and-reset. Same flag, different side effects.
  4. park() returned in microseconds with the flag still set. It doesn't throw, so code built on park must check the flag itself. That's exactly what BlockingQueue.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.

Fig. 2

When you catch InterruptedException, propagate it if your method can declare it, otherwise restore the flag and exit; swallowing it is the bug

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

Practise this topicThread basics: interview questions & scenarios
Further reading (17)