Thread.interrupt()深度解析:中断标志、InterruptedException与响应
Thread.interrupt()深度解析:中断标志、InterruptedException与响应
适读人群:Java后端开发者、需要优雅停止线程的工程师 | 阅读时长:约15分钟
开篇故事
2021年底,我们的批处理服务需要实现优雅停机。收到SIGTERM信号后,要在30秒内完成正在处理的任务,然后退出。
第一版的停机逻辑是直接调用Thread.stop()——已被废弃的API。同事小赵说没事,能用。结果测试时发现,某些情况下线程突然停止,导致正在写入的数据只写了一半,数据库出现脏数据。
Thread.stop()被废弃的原因就在这里:它强制中断线程,不给线程任何清理的机会,会破坏对象状态。
换成了Thread.interrupt(),但用的方式也有问题——没有在任务代码里检查中断标志,interrupt根本没用。
花了一个下午把中断机制彻底研究清楚,才写出了正确的优雅停机逻辑。今天把这套机制讲清楚。
一、Thread.interrupt()做了什么
1.1 中断不是强制停止
Thread.interrupt()做的事情是:
- 将目标线程的中断状态(interrupt flag)设为true
- 如果目标线程正在阻塞于某些方法(
sleep,wait,join,park等),抛出InterruptedException并清除中断标志
注意:interrupt()本身不会停止线程执行。它只是"发送了一个中断信号",线程是否响应,完全取决于线程自己的代码。
1.2 中断状态的读取
| 方法 | 含义 | 清除中断标志 |
|---|---|---|
Thread.interrupted() | 静态方法,检查当前线程的中断状态 | 是(重置为false) |
thread.isInterrupted() | 实例方法,检查目标线程的中断状态 | 否 |
1.3 响应中断的两种方式
方式1:传播InterruptedException(推荐)
public void doWork() throws InterruptedException {
Thread.sleep(1000); // 如果被中断,抛InterruptedException
}方式2:检查中断标志
public void doWork() {
while (!Thread.currentThread().isInterrupted()) {
// 执行一批工作
}
}二、中断机制深度解析
2.1 InterruptedException被抛出时中断标志被清除
这是最容易踩坑的地方。
当sleep()/wait()/join()因为中断而抛出InterruptedException时,线程的中断标志自动被清除(重置为false)。
这意味着:如果你在catch块里把InterruptedException吞掉(catch(InterruptedException e) {}),调用方再也无法知道这个线程曾经被中断过,中断信号丢失了。
正确处理方式:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置中断标志
// 然后决定是继续工作还是退出
}2.2 LockSupport.park()的中断响应
LockSupport.park()也响应中断,但不抛出InterruptedException,只是直接返回。调用者需要检查中断标志:
LockSupport.park();
if (Thread.interrupted()) {
// 是被中断唤醒的,而不是被unpark唤醒的
}AQS内部的parkAndCheckInterrupt()就是这样实现的:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted(); // 返回并清除中断标志
}三、完整代码实现
3.1 优雅停机的完整实现
package com.laozhang.concurrent.interrupt;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 优雅停机实现:使用interrupt机制
*
* 场景:批处理服务,收到停机信号后,
* 1. 停止接收新任务
* 2. 等待当前任务完成(最多30秒)
* 3. 超时则强制终止,记录未完成任务
*
* 测试环境:JDK 11
*/
public class GracefulShutdownDemo {
private volatile boolean running = true;
private final BlockingQueue<String> taskQueue = new LinkedBlockingQueue<>(1000);
private final ExecutorService workerPool;
private final AtomicBoolean shutdownInitiated = new AtomicBoolean(false);
public GracefulShutdownDemo(int workerCount) {
this.workerPool = new ThreadPoolExecutor(
workerCount, workerCount,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100),
r -> {
Thread t = new Thread(r, "worker-" + System.nanoTime());
t.setDaemon(false); // 非守护线程,JVM不会强制退出
return t;
}
);
}
/**
* 工作线程的任务:正确响应中断
*/
class WorkerTask implements Runnable {
@Override
public void run() {
while (running && !Thread.currentThread().isInterrupted()) {
String task = null;
try {
// poll而不是take,防止无限阻塞
task = taskQueue.poll(500, TimeUnit.MILLISECONDS);
if (task != null) {
processTask(task);
}
} catch (InterruptedException e) {
// 重新设置中断标志,然后退出循环
Thread.currentThread().interrupt();
System.out.printf("[%s] 收到中断信号,处理完当前任务后退出%n",
Thread.currentThread().getName());
break;
}
}
// 排空队列(超时30秒内的剩余任务)
System.out.printf("[%s] 工作线程退出%n", Thread.currentThread().getName());
}
private void processTask(String task) {
try {
System.out.printf("[%s] 处理任务:%s%n",
Thread.currentThread().getName(), task);
Thread.sleep(100); // 模拟处理耗时
} catch (InterruptedException e) {
// 任务处理中被中断,重置标志,让外层循环检测到
Thread.currentThread().interrupt();
}
}
}
/**
* 提交任务
*/
public boolean submitTask(String task) {
if (shutdownInitiated.get()) {
System.out.println("已停机,拒绝任务:" + task);
return false;
}
return taskQueue.offer(task);
}
/**
* 启动工作线程
*/
public void start(int workerCount) {
for (int i = 0; i < workerCount; i++) {
workerPool.submit(new WorkerTask());
}
}
/**
* 优雅停机:停止接收新任务,等待当前任务完成
*/
public void shutdown(long timeout, TimeUnit unit) {
if (!shutdownInitiated.compareAndSet(false, true)) {
return; // 已经在停机流程中
}
System.out.println("[Shutdown] 开始优雅停机...");
running = false; // 通知工作线程不再接受新任务
workerPool.shutdown(); // 停止接受新工作提交
try {
System.out.println("[Shutdown] 等待工作线程完成(最多" + timeout + " " + unit + ")...");
if (!workerPool.awaitTermination(timeout, unit)) {
System.out.println("[Shutdown] 超时,强制终止剩余线程...");
workerPool.shutdownNow(); // 向所有工作线程发送interrupt()
// 再等一小段时间
workerPool.awaitTermination(5, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
System.out.println("[Shutdown] 等待被中断,强制终止");
workerPool.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("[Shutdown] 停机完成,队列剩余任务数:" + taskQueue.size());
}
public static void main(String[] args) throws InterruptedException {
GracefulShutdownDemo service = new GracefulShutdownDemo(4);
service.start(4);
// 提交20个任务
for (int i = 0; i < 20; i++) {
service.submitTask("TASK-" + i);
}
// 3秒后触发停机
Thread.sleep(3000);
System.out.println("\n=== 触发优雅停机 ===");
service.shutdown(10, TimeUnit.SECONDS);
}
}3.2 中断标志的正确处理模式
package com.laozhang.concurrent.interrupt;
import java.util.concurrent.TimeUnit;
/**
* 中断处理的正确与错误模式对比
*
* 重点演示:
* 1. 吞掉InterruptedException的危害
* 2. 重新设置中断标志的重要性
* 3. 循环中检查isInterrupted()
*
* 测试环境:JDK 11
*/
public class InterruptHandlingPatterns {
// ===== 错误模式1:吞掉InterruptedException =====
static void wrongPattern1() throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// 错误!吞掉了中断,调用者不知道线程被中断了
// 中断标志被清除,后续代码无法感知
System.out.println("错误模式1:中断被忽略了");
}
// 线程继续运行(如果有后续代码)
System.out.println("继续运行...");
});
t.start();
Thread.sleep(100);
t.interrupt();
t.join();
System.out.println("t.isInterrupted() = " + t.isInterrupted());
}
// ===== 正确模式1:重新设置中断标志 =====
static void correctPattern1() throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置!
System.out.println("正确模式1:中断标志已重新设置");
}
// 检查中断标志,决定是否继续
if (Thread.currentThread().isInterrupted()) {
System.out.println("检测到中断,退出");
return;
}
});
t.start();
Thread.sleep(100);
t.interrupt();
t.join();
}
// ===== 正确模式2:在循环中响应中断 =====
static void correctPattern2() throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 每次循环检查中断标志
doUnitOfWork();
}
System.out.println("正确模式2:线程因中断退出");
});
t.start();
Thread.sleep(500);
t.interrupt();
t.join();
}
// ===== 正确模式3:可中断的长时间操作 =====
static void correctPattern3() throws InterruptedException {
Thread t = new Thread(() -> {
// 分批处理,每批检查一次中断
for (int batch = 0; batch < 100; batch++) {
if (Thread.currentThread().isInterrupted()) {
System.out.printf("正确模式3:第%d批时检测到中断,退出%n", batch);
return;
}
processBatch(batch);
}
});
t.start();
Thread.sleep(250);
t.interrupt();
t.join();
}
static void doUnitOfWork() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
static void processBatch(int batch) {
try {
Thread.sleep(100); // 模拟处理
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 错误模式1 ===");
wrongPattern1();
System.out.println("\n=== 正确模式1 ===");
correctPattern1();
System.out.println("\n=== 正确模式2 ===");
correctPattern2();
System.out.println("\n=== 正确模式3 ===");
correctPattern3();
}
}四、踩坑实录
坑1:在循环里用sleep然后吞InterruptedException,线程无法停止
报错现象: 调用thread.interrupt()后,线程没有停下来,继续无限循环。
原因分析:
// 问题代码
while (true) {
doWork();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 没有重新设置中断标志!
}
// 循环继续,线程无法停止
}每次sleep被中断,InterruptedException被catch住但没有重新设置中断标志。下次循环的while(true)是无条件的,线程永远不会停。
解法:
while (!Thread.currentThread().isInterrupted()) {
doWork();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置,让while条件生效
}
}坑2:ExecutorService.shutdownNow()返回的未执行任务丢失
报错现象: 调用shutdownNow()后,有些已提交但未执行的任务悄悄丢失,没有任何日志。
原因分析: shutdownNow()返回一个List<Runnable>,包含所有已提交但还未开始执行的任务。如果不处理这个返回值,这些任务就丢失了。
// 错误:丢失未执行的任务
executor.shutdownNow();
// 正确:保存并处理未执行的任务
List<Runnable> unexecuted = executor.shutdownNow();
if (!unexecuted.isEmpty()) {
log.warn("停机时丢弃了{}个未执行的任务", unexecuted.size());
// 可以写入数据库或持久化,以便重启后恢复
for (Runnable r : unexecuted) {
handleUnexecutedTask(r);
}
}坑3:interrupted()和isInterrupted()的混淆
报错现象: 在某个工具方法里用了Thread.interrupted()检查中断状态,导致调用方法后中断标志被清除,外层循环的中断检查失效。
原因分析: Thread.interrupted()(静态方法)调用后清除中断标志,而Thread.currentThread().isInterrupted()(实例方法)不清除。
在框架代码或工具方法里调用Thread.interrupted()会"偷走"中断标志,外层代码就感知不到中断了。
// 工具方法
boolean checkSomething() {
return Thread.interrupted(); // 危险!清除了中断标志
}
// 外层代码
while (!Thread.currentThread().isInterrupted()) {
if (checkSomething()) {
// ...
}
// 下次循环的isInterrupted()已经是false了(被工具方法清除)
}解法: 工具方法里用isInterrupted(),只有在明确"消费"这个中断信号时才用interrupted()。
坑4:Thread.sleep(0)不能代替检查中断
报错现象: 以为Thread.sleep(0)会触发中断检查,实际上不会(或行为不确定)。
原因分析: Thread.sleep(0)的行为在不同JVM实现上可能不同。在某些实现上,它会让出CPU但不检查中断;在某些实现上,它会抛出InterruptedException。
解法: 需要中断点时,明确调用Thread.sleep(1)(确保会响应中断),或显式检查Thread.currentThread().isInterrupted()。不要依赖sleep(0)的中断行为。
五、总结与延伸
Thread.interrupt()的使用原则:
interrupt()是协作式的:只是设置标志,不强制停止。被中断线程必须主动检查并响应。
InterruptedException抛出时中断标志被清除:catch后必须
Thread.currentThread().interrupt()重新设置,除非你明确"消费"了这个中断(如任务取消逻辑的最外层)。正确的中断响应模式:
- 可阻塞方法:
throws InterruptedException传播 - 循环:
while (!Thread.currentThread().isInterrupted()) - 耗时操作:分批处理,每批检查中断
- 可阻塞方法:
ExecutorService.shutdownNow():底层就是向所有工作线程调用
interrupt(),需要工作线程代码正确响应中断。
JDK 21虚拟线程的中断:虚拟线程同样支持interrupt()机制,语义与平台线程一致。当虚拟线程在阻塞时收到中断,会抛InterruptedException并清除中断标志。
