线程中断机制:优雅停止线程的艺术🛑

44 阅读8分钟

强制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)✅

核心思想:协作式中断

不是强制停止,而是:

  1. 主线程设置"中断标志"
  2. 工作线程主动检查标志
  3. 工作线程自己决定如何响应

生活类比:

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()

解决方案:

  1. 关闭流:socket.close()(会抛出SocketException)
  2. 使用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

  1. 主动检查:循环中定期检查中断标志
  2. 正确传播:catch后要么抛出,要么恢复标志
  3. 清理资源:中断前执行finally清理
  4. 使用lockInterruptibly:替代synchronized
  5. 优雅退出:给线程清理的机会

❌ DON'T

  1. ❌ 不要吞掉InterruptedException
  2. ❌ 不要用stop()/suspend()/resume()
  3. ❌ 不要在finally中清除中断标志
  4. ❌ 不要忽略interrupted()会清除标志
  5. ❌ 不要在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问题:看起来没变,其实变了又变回来!如何解决?🔄