前言

在 Java 并发编程中,“线程中断”(Thread Interruption)是一个高频出现但常被误解的概念。很多开发者误以为调用 interrupt() 就是“杀死”线程,实际上,Java 的中断机制是一种协作式的协议,而非抢占式的命令。

本文将从底层原理、API 辨析、状态交互以及 JUC 框架应用四个维度,带你彻底搞懂 Java 线程中断。

一、 中断的本质:从“命令”到“协商”

1. 为什么废弃 Thread.stop()

Java 早期版本曾提供 Thread.stop() 方法来强制终止线程。这就像 Linux 的 kill -9,它不给线程任何喘息或清理资源的机会,会立即释放线程持有的所有锁。这极易导致数据不一致(例如:转账操作只执行了一半就终止),因此在 JDK 1.2 中即被标记为废弃。

2. 协作式设计

Java 选择了**协作式(Cooperative)**中断机制。

  • 动作: 当调用 t.interrupt() 时,JVM 仅仅是将线程 t 内部的一个布尔类型**“中断标志位”置为 true**。
  • 结果: 线程 t 是立即停止、稍后停止、还是完全忽略这个信号,完全取决于线程 t 自身的代码逻辑。

这意味着,如果你的线程在执行死循环且不检查中断状态,哪怕外界调用一万次 interrupt(),该线程也会一直运行下去。

二、 API 深度辨析:三大金刚

Thread 类提供了三个名字极其相似的方法,混淆它们是 Bug 的源头。

方法 类型 作用 副作用(关键)
interrupt() 实例方法 设置中断标志位为 true
isInterrupted() 实例方法 查询当前中断状态 无 (只读)
interrupted() 静态方法 查询当前执行线程的状态 有 (查询并清除状态)

核心坑点: Thread.interrupted() 是一个静态方法,它检查的是当前运行代码的线程,并且具有“阅后即焚”的特性——一旦返回 true,它会立即将标志位重置为 false。这通常用于防止中断信号被重复消费。

三、 不同状态下的中断响应

理解中断的关键,在于明白线程处于不同状态时,对中断信号的反应是完全不同的。

1. 阻塞状态 (Sleep/Wait/Join)

当线程调用 Thread.sleep(), Object.wait(), Thread.join() 处于阻塞时:

  • 行为: 线程会被立即唤醒。
  • 副作用: 中断标志位被自动清除(置为 false)
  • 结果: 抛出 InterruptedException

这也是为什么我们在 catch 块中捕获到异常时,往往发现 isInterrupted() 返回的是 false

2. 运行状态 (Runnable)

如果线程正在进行 CPU 密集型计算:

  • 行为: 线程继续运行,不受任何影响。
  • 结果: 仅仅是中断标志位变为了 true
  • 对策: 必须在代码中手动轮询 if (Thread.currentThread().isInterrupted()) 来响应。

3. 阻塞状态 (Synchronized/BLOCKED)

当线程因为竞争 synchronized 锁失败而阻塞时:

  • 行为: 完全无法响应中断,线程会一直死等直到获取锁。
  • 结果: 中断标志位被设置为 true,但线程只有在拿到锁并在之后执行代码时才能感知到。

四、 JUC 框架中的中断处理

为了解决 synchronized 无法响应中断的问题,JUC(java.util.concurrent)引入了更灵活的机制。

1. LockSupport 的基石作用

JUC 锁的底层基于 LockSupport.park() 挂起线程。

  • park() 响应中断的方式是:立即唤醒,但不抛异常,也不清除标志位
  • 这就要求上层组件(如 AQS)在唤醒后必须主动检查中断状态。

2. Lock 接口的双重策略

Lock 接口提供了两种获取锁的方式,分别对应两种中断策略:

  • lock() (不响应中断):
    在排队等待锁的过程中,如果收到中断信号,它会记录下来(interrupted = true),但继续排队。直到拿到锁之后,再通过 selfInterrupt() 补上中断标记。适合必须执行完的任务。

  • lockInterruptibly() (响应中断):
    在排队过程中,一旦收到中断信号,立即抛出 InterruptedException 并放弃获取锁。适合防死锁或可取消的任务。

五、 最佳实践与设计模式

1. 绝对禁忌:生吞中断

永远不要写出这样的代码:

1
2
3
4
5
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace(); // 错误!异常被吞,上层毫不知情
}

由于 InterruptedException 抛出时标志位已被清除,如果不处理,上层调用者将无法得知线程曾被要求停止。

2. 正确姿势

  • 方案 A(推荐): 方法签名抛出 InterruptedException,将责任甩给上层。
  • 方案 B(补救):catch 块中调用 Thread.currentThread().interrupt()恢复中断状态

3. 两阶段终止模式 (Two-Phase Termination)

处理后台线程(如监控采集)的标准模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void run() {
while (!Thread.currentThread().isInterrupted()) { // 1. 检查标志位
try {
doWork();
Thread.sleep(5000); // 阻塞点
} catch (InterruptedException e) {
// 2. 捕获异常后,重新设置标志位
Thread.currentThread().interrupt();
// 退出循环
break;
}
}
cleanUp(); // 资源释放
}

结语

Java 的中断机制是构建稳健并发程序的基石。从 Thread.stop 的废弃到 JUC 的精细化控制,体现了 Java 对数据安全和线程协作的重视。掌握这一机制,不仅能让你写出更优雅的代码,更是进阶阅读 JDK 源码的必经之路。