Coordination, Waiting & Cancellation
Swallowed Interrupt During Shutdown
Swallowed Interrupt During Shutdown: practice a Java concurrency bug with symptoms like Shutdown hangs, Worker ignores cancellation, Process still alive....
- Cancellation
- Cancellation
- Interrupts
- Java
- Beginner
Production symptoms
- Shutdown hangs
- Worker ignores cancellation
- Process still alive
Failure scenario
Code
class Poller implements Runnable {
private final BlockingQueue<Job> jobs;
public void run() {
while (true) {
try {
Job job = jobs.take();
process(job);
} catch (InterruptedException ignored) {
// Cancellation signal is lost; the loop waits again.
}
}
}
}
Prod Symptoms
A deployment or service shutdown interrupts a background worker, but the worker catches the signal and returns to its polling loop.
Key signal: Interrupt requests cooperative cancellation; it does not forcibly stop a thread. Swallowing the signal lets the task continue.
- Shutdown is requested, but the executor does not terminate
- awaitTermination returns false or the process exceeds its shutdown budget
- The worker logs an interrupt and then appears again in sleep(), wait(), or BlockingQueue.take()
- CPU stays low because the worker is blocked again, not spinning
- The issue appears during shutdown or redeploy, while normal processing looks healthy
Run Locally
- The worker enters an interruptible sleep
- shutdownNow interrupts the worker
- Thread.sleep throws InterruptedException and clears the interrupt status
- The catch block ignores the cancellation request
- The worker starts another loop iteration and sleeps again
- awaitTermination returns false
- The JVM remains alive until you stop the process
What to look for
- A pool thread still alive after shutdown
- A catch block for InterruptedException that only logs, ignores, or continues
- Thread dumps showing the worker back in sleep, wait, or BlockingQueue.take after cancellation
javac SwallowedInterruptShutdownDemo.java
java SwallowedInterruptShutdownDemo
jps
jstack <pid>
jcmd <pid> Thread.print
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
public class SwallowedInterruptShutdownDemo {
public static void main(String[] args) throws Exception {
ExecutorService worker = Executors.newSingleThreadExecutor(named("poller"));
worker.submit(() -> {
while (true) {
try {
System.out.println("poller waiting for work");
Thread.sleep(10_000);
} catch (InterruptedException ignored) {
System.out.println("poller swallowed interrupt and keeps running");
}
}
});
Thread.sleep(500);
System.out.println("shutdownNow sends interrupt");
worker.shutdownNow();
boolean terminated = worker.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("terminated = " + terminated);
if (!terminated) {
System.out.println("Bug reproduced. Use Ctrl+C to stop the process.");
}
}
private static ThreadFactory named(String name) {
return runnable -> new Thread(runnable, name);
}
}
Note: shutdownNow interrupts the sleeping worker. Thread.sleep throws InterruptedException and clears the interrupt status, but the catch block continues the loop and sleeps again.
Diagnosis and fix
Explanation
Interrupt is a cooperative signal whose meaning is defined by the task's cancellation contract.
Key signal: Catching InterruptedException is a cancellation decision, not ordinary error handling.
- shutdownNow attempts to interrupt actively executing executor tasks
- sleep(), wait(), join(), and interruptible BlockingQueue operations can report interruption with InterruptedException
- Throwing InterruptedException clears the thread's interrupted status
- If the catch block continues without exiting, propagating, or restoring the signal, cancellation is lost
- Restoring the status preserves the signal, but the task must still stop when interruption means cancellation
- Not every blocking API is interruptible, so some resources require their own cancellation mechanism
How to Diagnose
Start with the shutdown timeline, then trace the cancellation signal into the worker.
- Confirm that lifecycle code called shutdownNow(), Future.cancel(true), or Thread.interrupt()
- Check whether awaitTermination exceeded the shutdown budget
- Correlate the interrupt or shutdown log with a worker that later returns to sleep(), wait(), or queue polling
- Inspect catch blocks that log or ignore InterruptedException and continue looping
- Remember that InterruptedException clears the status, so a later thread dump may show an ordinary waiting thread rather than evidence of the earlier interrupt
- Verify whether the blocking operation actually supports interruption
jps
jstack <pid>
jcmd <pid> Thread.print
shutdownNow sends interrupt
poller swallowed interrupt and keeps running
terminated = false
Bug reproduced. Use Ctrl+C to stop the process.
Note: A thread dump shows that the worker is still alive, but logs and code review are needed to prove that an earlier interrupt was swallowed.
How to Fix
- Exit the task when interruption means cancellation
- Propagate InterruptedException when the caller can decide how to handle cancellation
- When propagation is impossible, restore the status with Thread.currentThread().interrupt()
- Do not restore the status and then continue an unconditional polling loop
- Treat continuing after interruption as an explicit, documented policy
- Keep awaitTermination bounded so failed shutdown remains observable
- Use the cancellation mechanism appropriate for blocking operations that do not respond to interrupt
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
public class InterruptAwareShutdownFixed {
public static void main(String[] args) throws Exception {
ExecutorService worker = Executors.newSingleThreadExecutor(named("poller"));
worker.submit(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("poller waiting for work");
Thread.sleep(10_000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("poller observed interrupt and stops");
return;
}
});
Thread.sleep(500);
System.out.println("shutdownNow sends interrupt");
worker.shutdownNow();
boolean terminated = worker.awaitTermination(2, TimeUnit.SECONDS);
System.out.println("terminated = " + terminated);
}
private static ThreadFactory named(String name) {
return runnable -> new Thread(runnable, name);
}
}
Note: The fixed worker treats interruption as a shutdown signal, restores the interrupted status, and lets the task finish.