你真的懂Thread.currentThread().interrupt()吗?

引言

在很多开源代码中都会看到Thread.currentThread().interrupt()这串代码,可是你真的了解它有什么用处吗?今天我们就一起来看看这串代码到底是干嘛的。

以下这串代码是Spring AI Alibaba的ModelRetryInterceptor类的片段。

java 复制代码
try {
    log.info("Retry after {} ms", currentDelay);
    Thread.sleep(currentDelay);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new RuntimeException("Retry interrupted", e);
}

消失的信号 -- 线程的中断标记位

要理解这个问题,首先要明白 Java 的中断(Interruption)是一种协作机制,而不是强制命令。它依赖于线程对象内部的一个布尔标记位(Interrupt Status)。

当我们调用 t.interrupt() 时,仅仅是把这个标记位设为 true

然而,Java 的阻塞库函数(如 Thread.sleep(), Object.wait(), BlockingQueue.take())对中断非常敏感。它们的处理逻辑通常包含以下两个原子步骤:

  1. 检测到中断信号 :发现中断标记位为 true
  2. 清除标记并抛出异常 :为了响应这个中断,它们会先将标记位重置为 false ,然后抛出 InterruptedException

这就是问题的核心: 当你捕获到 InterruptedException 时,线程的中断标记位已经被 JVM 自动"吃掉"了(重置为 false)。

如果你在 catch 块中什么都不做(Swallow the exception),或者只是简单打印日志,那么线程看起来就像从未被中断过一样。

结果 -- "僵尸线程"

让我们看一个典型的生产环境 Bug。假设你需要在一个线程中不断处理任务,直到线程池关闭:

java 复制代码
public void run() {
    // 依靠中断状态来判断是否结束
    while (!Thread.currentThread().isInterrupted()) { 
        try {
            // 模拟执行任务
            doWork();
            // 模拟阻塞
            Thread.sleep(1000); 
        } catch (InterruptedException e) {
            //  错误做法:只是打印日志,没有恢复状态
            System.out.println("收到停止信号,但我把信号搞丢了...");
        }
    }
    System.out.println("线程退出");
}

发生了什么?

  1. 外部调用 future.cancel(true)executor.shutdownNow(),发送中断信号。
  2. sleep() 此时被唤醒,它清除标志位 (变回 false),抛出异常。
  3. 进入 catch 块,打印日志。
  4. 代码继续运行,回到 while 循环头部。
  5. 检查 !isInterrupted(),由于刚才标志位被清除了,这里返回 true(即认为没有被中断)。
  6. 线程继续死循环执行,无法停止。

这就是所谓的僵尸线程------你以为你把它杀死了,但它只是"震动"了一下(抛了个异常),然后像没事人一样继续跑。

正确的姿势 -- 恢复现场

为了解决这个问题,我们需要在捕获异常后,手动恢复刚才丢失的中断状态。

Java 复制代码
public void run() {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            doWork();
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("被中断,准备退出...");
            
            // 正确做法:重新设置中断状态
            Thread.currentThread().interrupt(); 
            
            // 通常配合 break 跳出循环
            break; 
        }
    }
}

这行代码的作用就是告诉后续的代码(包括 while 判断,或者调用栈上层的代码):"刚才发生过中断,虽然异常把标志位清除了,但我现在把它补回来。 "

那你可能有疑问?为啥我不直接抛出异常?你可以抛出异常,但是分场景来。

  • 可以抛出时: 如果你的方法签名允许抛出(例如你写的是普通业务方法),那优先选择抛出异常,这是最正确的做法,让上层决定如何处理。

  • 不能抛出时: 在实现 Runnable.run() 接口时,run 方法的签名是固定的,不允许抛出受检异常 。你必须在内部 try-catch。在这种情况下,你捕获了异常,就必须负责恢复状态,以便调用栈上层的代码能感知到"哦,原来有人想让我停止"

时序问题

我们通过一段代码来验证我们所说的问题,体验瞬间的变化:

java 复制代码
Thread t = new Thread(() -> {
    try {
        System.out.println("1. 准备睡觉");
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        // 当我们进入这里时,JVM 已经把标志位清除了
        System.out.println("3. 捕获异常,此时标志位是: " + Thread.currentThread().isInterrupted()); // 输出 false
        
        // 所以我们需要手动"恢复"
        Thread.currentThread().interrupt();
        System.out.println("4. 手动恢复后,标志位是: " + Thread.currentThread().isInterrupted()); // 输出 true
    }
});

t.start();
Thread.sleep(100); // 确保子线程已经睡着了

System.out.println("2. 主线程调用 interrupt");
t.interrupt(); // 这一瞬间,t 的标志位其实是 true,但很快就被 sleep 内部逻辑清除了

假设线程 A 正在 Thread.sleep(10000)

  1. Step 1: 其他线程动作 其他线程调用 threadA.interrupt()

    • 结果: 此时,线程 A 的中断标志位瞬间变为 true
  2. Step 2: 线程 A (JVM 内部机制) 响应 线程 A 处于 sleep 状态,JVM 内部机制检测到了标志位变成了 true

  3. Step 3: 清除状态 (关键步骤) JVM 决定唤醒线程 A。但在唤醒并抛出异常之前 ,JVM 会做一个动作:将中断标志位重置(清除)为 false

    • 为什么要清除? 就像你设定了一个闹钟,闹钟响了(抛出异常),你醒来后第一件事是按下闹钟的停止键(清除标志),否则它会一直响。
  4. Step 4: 抛出异常 JVM 抛出 InterruptedException,控制权进入你的 catch 块。

    • 结果: 当代码运行到 catch 块里面时,你如果去查 isInterrupted(),看到的就是 false

总结

永远不要生吞(Swallow)中断异常,除非你明确知道你的线程设计就是为了忽略停止信号

相关推荐
橘色的狸花猫2 小时前
简历与岗位要求相似度分析系统
java·nlp
独自破碎E2 小时前
Leetcode1438绝对值不超过限制的最长连续子数组
java·开发语言·算法
用户91743965392 小时前
Elasticsearch Percolate Query使用优化案例-从2000到500ms
java·大数据·elasticsearch
程序员NEO2 小时前
LangChain4j 工具调用实战
后端
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue小区人脸识别门禁系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
yaoxin5211233 小时前
279. Java Stream API - Stream 拼接的两种方式:concat() vs flatMap()
java·开发语言
左灯右行的爱情3 小时前
Kafka专辑- 整体架构
分布式·架构·kafka
萧曵 丶3 小时前
订单超时解决方案详解
面试·架构·高并发·大厂
坚持学习前端日记3 小时前
2025年的个人和学习年度总结以及未来期望
java·学习·程序人生·职场和发展·创业创新