强制kill线程是野蛮人,优雅中断才是绅士!让我们学会如何礼貌地告诉线程:"该下班了"。
一、开场:停止线程的血泪史😭
远古时代:Thread.stop()(已废弃)
Thread thread = new Thread(() -> {
// 正在处理重要数据
updateDatabase();
});
thread.start();
thread.stop(); // ☠️ 直接杀死线程!
问题:
- 💥 立即终止,不管在干什么
- 💣 释放所有锁,可能导致数据不一致
- 🔥 资源泄漏,finally块不执行
例子:转账被stop()中断
public synchronized void transfer(Account from, Account to, int amount) {
from.balance -= amount; // ① 执行完
// ☠️ 这里被stop(),锁被释放!
to.balance += amount; // ② 没执行,钱丢了!
}
结果: from的钱减少了,to的钱没增加,凭空消失💸
二、现代方案:中断机制(Interruption)✅
核心思想:协作式中断
不是强制停止,而是:
- 主线程设置"中断标志"
- 工作线程主动检查标志
- 工作线程自己决定如何响应
生活类比:
stop()像什么?
- 老板冲进会议室,拽着你就走:"不开了!"
- 会议记录写了一半,资料散落一地
interrupt()像什么?
- 老板敲门说:"有急事,能结束吗?"
- 你保存资料,整理桌面,礼貌退出
三、中断API:三个核心方法🔑
1. interrupt() - 设置中断标志
Thread thread = new Thread(() -> {
// 工作中...
});
thread.start();
thread.interrupt(); // 请求中断
作用:
- 设置线程的中断标志为
true - 如果线程在
wait/sleep/join,会抛出InterruptedException
2. isInterrupted() - 检查中断标志
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
System.out.println("我被中断了");
}
特点:
- 不会清除中断标志
- 可以多次调用
3. interrupted() - 检查并清除中断标志
if (Thread.interrupted()) {
System.out.println("检测到中断,标志被清除");
}
// 第二次调用返回false(标志已清除)
if (Thread.interrupted()) {
System.out.println("不会执行");
}
特点:
- 检查中断标志
- 自动清除标志
- 静态方法,作用于当前线程
对比表
| 方法 | 作用 | 是否清除标志 | 所属 |
|---|---|---|---|
interrupt() | 设置中断标志 | - | 实例方法 |
isInterrupted() | 检查中断标志 | ❌ 不清除 | 实例方法 |
interrupted() | 检查中断标志 | ✅ 清除 | 静态方法 |
四、场景1:响应式任务(主动检查)🔍
基本模式
public class ResponsiveTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
doWork();
}
// 清理资源
cleanup();
System.out.println("任务已停止");
}
private void doWork() {
// 业务逻辑
System.out.println("正在工作...");
}
private void cleanup() {
// 清理资源
System.out.println("清理资源");
}
}
// 使用
Thread thread = new Thread(new ResponsiveTask());
thread.start();
Thread.sleep(1000);
thread.interrupt(); // 请求停止
输出:
正在工作...
正在工作...
正在工作...
清理资源
任务已停止
实战:文件扫描器
public class FileScanner implements Runnable {
private final Path directory;
private volatile int scannedFiles = 0;
public FileScanner(Path directory) {
this.directory = directory;
}
@Override
public void run() {
try {
Files.walk(directory)
.forEach(path -> {
// 检查中断
if (Thread.currentThread().isInterrupted()) {
throw new RuntimeException("扫描被中断");
}
// 扫描文件
scanFile(path);
scannedFiles++;
});
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("扫描完成,共" + scannedFiles + "个文件");
}
private void scanFile(Path path) {
// 模拟扫描
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// 恢复中断标志
Thread.currentThread().interrupt();
}
}
}
五、场景2:阻塞方法的中断💤
阻塞方法会响应中断
响应中断的方法:
Thread.sleep()Object.wait()Thread.join()BlockingQueue.take()/put()Lock.lockInterruptibly()Condition.await()Selector.select()(NIO)
特点: 这些方法在中断时会抛出InterruptedException
sleep()的中断
public class SleepInterruption {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
System.out.println("准备睡眠10秒...");
Thread.sleep(10_000); // 睡10秒
System.out.println("睡醒了"); // 不会执行
} catch (InterruptedException e) {
System.out.println("睡眠被中断!");
// 中断标志已被清除
System.out.println("中断标志:" + Thread.currentThread().isInterrupted()); // false
}
});
thread.start();
Thread.sleep(1000); // 等1秒
thread.interrupt(); // 中断睡眠
}
}
输出:
准备睡眠10秒...
睡眠被中断!
中断标志:false
关键: InterruptedException抛出时,中断标志会被自动清除!
为什么要清除中断标志?
因为异常已经传递了中断信息,不需要标志了。
但如果你捕获后要传递中断:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 恢复中断标志
Thread.currentThread().interrupt();
throw new RuntimeException("任务被中断", e);
}
六、场景3:不响应中断的阻塞❌
哪些方法不响应中断?
- synchronized - 等待锁时不响应中断
- InputStream.read() - 阻塞IO不响应
- SocketInputStream.read() - Socket阻塞读不响应
synchronized不响应中断
public class SynchronizedNoInterrupt {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(10_000); // 持有锁10秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
System.out.println("t2尝试获取锁...");
synchronized (lock) { // 在这里阻塞
System.out.println("t2获得锁"); // 不会执行
}
});
t1.start();
Thread.sleep(100);
t2.start();
Thread.sleep(100);
t2.interrupt(); // 中断t2(但t2还在等锁,不会响应!)
System.out.println("t2被中断,但还在等锁");
}
}
输出:
t2尝试获取锁...
t2被中断,但还在等锁
(t2一直阻塞在synchronized,直到t1释放锁)
解决方案: 用ReentrantLock.lockInterruptibly()
ReentrantLock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 可中断的加锁
// 业务逻辑
} catch (InterruptedException e) {
System.out.println("获取锁时被中断");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
阻塞IO不响应中断
InputStream in = socket.getInputStream();
int data = in.read(); // 阻塞,不响应interrupt()
解决方案:
- 关闭流:
socket.close()(会抛出SocketException) - 使用NIO:
SocketChannel(响应中断)
七、最佳实践:正确处理中断✅
实践1:不要吞掉InterruptedException
// ❌ 错误:吞掉异常
public void badExample() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 什么都不做,中断信息丢失!
}
}
// ✅ 正确:传播中断
public void goodExample() throws InterruptedException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 方案1:向上抛出
throw e;
}
}
// ✅ 正确:恢复中断标志
public void goodExample2() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 方案2:恢复标志
Thread.currentThread().interrupt();
// 继续处理
}
}
实践2:循环中正确检查中断
// ❌ 错误:只检查一次
public void process() {
if (Thread.interrupted()) {
return; // 退出
}
for (int i = 0; i < 1000000; i++) {
doWork(i); // 耗时操作,无法中断
}
}
// ✅ 正确:循环中检查
public void process() {
for (int i = 0; i < 1000000; i++) {
if (Thread.interrupted()) {
System.out.println("任务被中断");
return;
}
doWork(i);
}
}
实践3:清理资源
public void processWithCleanup() {
FileWriter writer = null;
try {
writer = new FileWriter("data.txt");
while (!Thread.currentThread().isInterrupted()) {
String data = produceData();
writer.write(data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 确保资源释放
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
八、实战:可中断的任务框架🛠️
public abstract class InterruptibleTask implements Runnable {
private volatile boolean cancelled = false;
@Override
public final void run() {
try {
// 前置检查
if (Thread.currentThread().isInterrupted()) {
return;
}
// 执行任务
doTask();
} catch (InterruptedException e) {
System.out.println("任务被中断");
Thread.currentThread().interrupt(); // 恢复标志
} finally {
// 清理资源
cleanup();
}
}
/**
* 子类实现具体任务
*/
protected abstract void doTask() throws InterruptedException;
/**
* 清理资源
*/
protected void cleanup() {
// 默认实现为空
}
/**
* 取消任务
*/
public void cancel() {
cancelled = true;
}
/**
* 检查是否应该停止
*/
protected void checkInterruption() throws InterruptedException {
if (cancelled || Thread.currentThread().isInterrupted()) {
throw new InterruptedException("任务被取消");
}
}
}
// 使用示例
public class DataProcessor extends InterruptibleTask {
@Override
protected void doTask() throws InterruptedException {
for (int i = 0; i < 100; i++) {
// 定期检查中断
checkInterruption();
// 处理数据
System.out.println("处理数据 " + i);
Thread.sleep(100);
}
}
@Override
protected void cleanup() {
System.out.println("清理资源");
}
}
// 测试
public class Test {
public static void main(String[] args) throws InterruptedException {
InterruptibleTask task = new DataProcessor();
Thread thread = new Thread(task);
thread.start();
Thread.sleep(1000); // 运行1秒
thread.interrupt(); // 中断
thread.join();
System.out.println("任务已停止");
}
}
九、线程池中的中断📦
ExecutorService的关闭
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
doWork();
}
});
// 方式1:优雅关闭(不接受新任务,等待现有任务完成)
executor.shutdown();
// 方式2:立即关闭(尝试中断所有任务)
List<Runnable> notStarted = executor.shutdownNow(); // 返回未执行的任务
// 方式3:取消单个任务
future.cancel(true); // true表示中断
Future.cancel()的参数
Future<?> future = executor.submit(task);
// mayInterruptIfRunning:
// - true: 如果任务正在运行,尝试中断
// - false: 如果任务还没开始,取消;如果已经开始,让它完成
boolean cancelled = future.cancel(true);
十、中断的传播链🔗
正确传播中断
public class InterruptionChain {
// 方法1:捕获并处理
public void method1() {
try {
method2(); // 调用其他方法
} catch (InterruptedException e) {
// 处理中断
System.out.println("方法1检测到中断");
// 恢复标志
Thread.currentThread().interrupt();
}
}
// 方法2:向上抛出
public void method2() throws InterruptedException {
method3(); // 继续传播
}
// 方法3:检测中断
public void method3() throws InterruptedException {
if (Thread.interrupted()) {
throw new InterruptedException("检测到中断");
}
// 或者
Thread.sleep(1000); // 会自动抛出
}
}
转换为RuntimeException
public class TaskRunner {
public void runTask() {
try {
longRunningTask();
} catch (InterruptedException e) {
// 转换为运行时异常
Thread.currentThread().interrupt(); // 恢复标志
throw new RuntimeException("任务被中断", e);
}
}
private void longRunningTask() throws InterruptedException {
// 耗时任务
}
}
十一、常见陷阱与错误⚠️
陷阱1:捕获后不处理
// ❌ 错误
while (true) {
try {
doWork();
Thread.sleep(100);
} catch (InterruptedException e) {
// 吞掉异常,继续循环!
e.printStackTrace();
}
}
问题: 中断被忽略,线程永远停不下来!
陷阱2:使用interrupted()后忘记重新设置
// ❌ 错误
if (Thread.interrupted()) { // 标志被清除了!
cleanup();
return;
}
// 后续代码以为没被中断
doMoreWork(); // 可能不应该执行
修复:
if (Thread.interrupted()) {
cleanup();
Thread.currentThread().interrupt(); // 恢复标志
return;
}
陷阱3:在finally块中清除中断标志
// ❌ 错误
try {
doWork();
} finally {
Thread.interrupted(); // 清除了中断标志!
}
问题: 调用者无法知道任务被中断了。
十二、性能考虑⚡
检查中断的频率
// ❌ 检查太频繁,性能差
for (int i = 0; i < 1_000_000; i++) {
if (Thread.interrupted()) return;
simpleOperation(); // 很快的操作
}
// ✅ 合理频率
for (int i = 0; i < 1_000_000; i++) {
if (i % 1000 == 0 && Thread.interrupted()) return;
simpleOperation();
}
建议: 在耗时操作前检查,快速操作可以批量检查。
十三、面试高频问答💯
Q1: interrupt()会立即停止线程吗?
A: 不会! 它只是设置中断标志,线程需要主动检查并响应。
Q2: InterruptedException抛出后,中断标志是什么状态?
A: 被清除(false)。如果需要传播中断,要手动调用Thread.currentThread().interrupt()恢复。
Q3: 为什么不能用stop()停止线程?
A: 因为stop()会:
- 立即释放所有锁(可能导致数据不一致)
- 不执行finally(资源泄漏)
- 不给线程清理的机会
Q4: synchronized块中如何响应中断?
A: synchronized不响应中断,可以:
- 改用
ReentrantLock.lockInterruptibly() - 或者定期检查标志
Q5: 如何中断阻塞IO?
A:
- 传统IO:关闭流(会抛异常)
- NIO:使用可中断的Channel
十四、总结:中断的正确姿势📝
✅ DO
- 主动检查:循环中定期检查中断标志
- 正确传播:catch后要么抛出,要么恢复标志
- 清理资源:中断前执行finally清理
- 使用lockInterruptibly:替代synchronized
- 优雅退出:给线程清理的机会
❌ DON'T
- ❌ 不要吞掉
InterruptedException - ❌ 不要用
stop()/suspend()/resume() - ❌ 不要在finally中清除中断标志
- ❌ 不要忽略
interrupted()会清除标志 - ❌ 不要在synchronized中依赖中断
最佳实践模板
public void task() {
try {
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑
doWork();
// 阻塞调用
Thread.sleep(100);
}
} catch (InterruptedException e) {
// 日志记录
logger.info("任务被中断");
// 恢复标志
Thread.currentThread().interrupt();
} finally {
// 清理资源
cleanup();
}
}
下期预告: CAS的ABA问题:看起来没变,其实变了又变回来!如何解决?🔄