Java线程停止之“外卖骑手历险记”

119 阅读13分钟

来一场关于Java线程停止的“外卖骑手历险记”!准备好爆米花,故事开始啦!

角色设定:

  • 小明:  一个充满干劲但有点莽撞的外卖骑手(代表一个正在运行的Thread)。
  • 外卖平台(Thread类):  管理着众多骑手。
  • 用户(调用代码的你):  上帝视角,可以给平台或骑手下命令。
  • 订单(Runnable任务):  骑手需要完成的工作。
  • 手机(中断标志位):  骑手用来接收平台指令。
  • 交通灯(线程阻塞点):  如sleep()wait()join(), 等待锁等。

第一幕:莽撞的“拔电源” - stop() 为何被封印?

场景:  小明正骑着电动车,一手拿着热腾腾的麻辣烫,一手拿着手机看导航,飞速赶往用户家。突然!

用户(着急地):  “哎呀!订单取消了!平台!快!立刻!马上!让小明停止!用stop()!”

外卖平台(无奈但执行):  “遵命!启动stop()终极指令!”

平台强行操作:  平台瞬间远程锁死了小明的电动车(相当于强制终止线程执行流)。后果是灾难性的:

  1. 麻辣烫飞了!  电动车突然锁死,小明手里的麻辣烫(对象状态)脱手飞出,摔得满地都是(对象状态被破坏,处于不一致状态)。
  2. 电动车横在路中间!  小明被锁在路中间(线程持有的锁,比如他正占用着一个公共自行车道的入口锁),其他骑手(其他线程)全被堵住了(死锁或资源竞争风险)。
  3. 用户差评!平台罚款!  用户没收到餐,平台信誉受损(程序崩溃、数据丢失、不可预测行为)。

java

public class DangerousStop {
    public static void main(String[][] args) throws InterruptedException {
        Thread rider = new Thread(() -> {
            try {
                System.out.println("骑手[小明]拿到麻辣烫,开始配送...");
                // 模拟配送过程(临界区,假设持有了某个锁)
                synchronized (this) {
                    System.out.println("骑手[小明]进入了小区门禁(持有锁)...");
                    Thread.sleep(2000); // 模拟在小区内行驶
                }
                System.out.println("骑手[小明]送达!"); // 永远执行不到了
            } catch (InterruptedException e) {
                // 即使这里捕获了InterruptedException, stop()也会无视它强行终止
                System.out.println("骑手[小明]被打断,但无法清理现场!");
            } finally {
                System.out.println("骑手[小明]的finally块:试图归还门禁卡..."); // 永远执行不到了!
            }
        });

        rider.start();
        Thread.sleep(1000); // 让小明骑一会儿
        rider.stop(); // !!! 极度危险 !!! 相当于远程锁死电动车
    }
}
// 可能输出:
// 骑手[小明]拿到麻辣烫,开始配送...
// 骑手[小明]进入了小区门禁(持有锁)...
// (然后停止,finally块没执行,锁没释放,对象状态可能不一致)

教训:  stop() 太粗暴了!它像直接拔电源,不给线程任何清理现场(释放锁、关闭文件、保存状态)的机会,极易导致数据损坏、死锁和不可预测的后果。所以它被  @Deprecated (废弃)  了,绝对不要使用!


第二幕:文明的“呼叫返航” - interrupt() 与中断标志

场景:  平台吸取了教训。这次,当用户取消订单时:

用户:  “平台,订单又取消了!这次文明点,通知小明中断配送!用interrupt()!”

外卖平台(发送指令):  “明白!向骑手[小明]发送interrupt()指令!”

平台操作:  平台给小明的手机(中断标志位)  发送了一条强提醒消息(设置线程的中断标志位为true)。

小明的反应(协作式中断):

  1. 正在骑车看路(线程正常运行):  小明感觉到手机震动(中断标志被置位) 。他是个负责任的骑手,会定期查看手机(在代码中主动检查Thread.currentThread().isInterrupted() 。看到“订单取消,立即返回”的消息,他决定:

    • 找个安全的地方靠边停车(安全地结束当前任务)。
    • 把麻辣烫(资源)妥善处理掉(比如送回商家,代表释放资源)。
    • 把小区门禁卡还了(释放锁)。
    • 然后骑车返回站点(线程正常退出run()方法)。
  2. 正在等红灯 (sleep(5000)):  小明在路口等红灯(线程在阻塞状态)。此时手机震动(中断信号来了)。神奇的事情发生了:

    • 红灯(sleep)瞬间变绿!  平台的特殊指令让交通灯(阻塞方法)立即抛出一个InterruptedException,仿佛绿灯提前亮了。
    • 小明被这个异常惊醒(捕获InterruptedException),立刻明白了:有中断!他同样需要安全靠边停车、处理麻辣烫、还门禁卡,然后返回(catch块中清理并退出)。注意:  当抛出InterruptedException时,平台会好心地把中断标志位重置回false(就像手机震动停了),所以小明通常需要在catch块里自己再调用Thread.currentThread().interrupt()  重新设置标志(或者直接退出,不检查也行,但最好保留中断意图)。
  3. 正在等一个永远不来的人 (wait()) 或者堵在另一个骑手后面 (lock.lock()):  类似等红灯的情况,这些阻塞方法也会被interrupt()唤醒并抛出InterruptedException,小明同样需要响应。

java

public class SafeInterrupt {
    public static void main(String[][] args) throws InterruptedException {
        Thread rider = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) { // 骑手定期看手机(检查中断标志)
                try {
                    System.out.println("骑手[小明]正常行驶中...");
                    Thread.sleep(1000); // 模拟骑行一段时间,可能被中断唤醒
                    // 或者在这里进行其他工作,并在关键点检查isInterrupted()
                } catch (InterruptedException e) {
                    System.out.println("骑手[小明]在等红灯时被中断指令唤醒!");
                    // 重要:恢复中断状态,因为sleep被中断后会清除标志位
                    Thread.currentThread().interrupt(); // 重新设置标志,让外面的循环也能看到
                    // 也可以选择在这里直接break退出循环
                }
            }
            // 安全清理工作
            System.out.println("骑手[小明]收到中断,安全靠边停车...");
            System.out.println("骑手[小明]处理麻辣烫(释放资源)...");
            System.out.println("骑手[小明]归还门禁卡(释放锁)...");
            System.out.println("骑手[小明]返回站点。");
        });

        rider.start();
        Thread.sleep(3000); // 让小明骑一会儿
        System.out.println("用户取消订单!平台发送中断指令...");
        rider.interrupt(); // 设置中断标志位
    }
}
// 输出可能类似于:
// 骑手[小明]正常行驶中...
// 骑手[小明]正常行驶中...
// 用户取消订单!平台发送中断指令...
// 骑手[小明]在等红灯时被中断指令唤醒!
// 骑手[小明]收到中断,安全靠边停车...
// 骑手[小明]处理麻辣烫(释放资源)...
// 骑手[小明]归还门禁卡(释放锁)...
// 骑手[小明]返回站点。

关键点:

  • interrupt() 不会强制停止线程,它只是设置一个标志位 (isInterrupted())  或者唤醒处于阻塞状态的线程(通过抛InterruptedException)。
  • 线程必须协作!它需要在适当的时候检查中断标志位正确处理InterruptedException,然后主动、安全地终止自己的执行。这就是协作式中断 (Cooperative Interruption)
  • 阻塞方法 (sleepwaitjoin, 等待锁等) 在收到interrupt()后会立即响应,抛出InterruptedException,并清除中断标志位。处理异常时通常需要恢复中断状态直接退出

第三幕:自己画个“停止牌” - volatile 标记中断能用吗?

场景:  用户想:“何必麻烦平台?我自己搞个volatile boolean stopFlag = false;,让小明自己看这个标志不就行了?”

小明的反应:

  1. 正在骑车看路:  小明会定期看路边的这个“停止牌” (if (stopFlag) break;)。只要牌翻到true,他就安全停车处理后续。这招在骑手一直清醒工作(线程不阻塞)时管用!
  2. 正在等红灯 (sleep(5000)):  糟了!小明专心等红灯,根本不看路边那个“停止牌”  (volatile变量)。即使用户把stopFlag设为true,小明也要傻等5秒红灯结束才会看到牌子。响应严重延迟!
  3. 正在等一个永远不来的人 (wait()):  同样,小明完全陷入等待状态,无视路边的“停止牌”。stopFlag设为true也没用,他醒不过来!

java

public class VolatileFlag {
    private static volatile boolean stopRequested = false;

    public static void main(String[][] args) throws InterruptedException {
        Thread rider = new Thread(() -> {
            while (!stopRequested) { // 只在运行时检查
                try {
                    System.out.println("骑手[小明]正常行驶中...");
                    Thread.sleep(1000); // 阻塞点!这里停住了
                } catch (InterruptedException e) {
                    // 不会被触发,因为我们没用interrupt()
                }
            }
            System.out.println("骑手[小明]看到停止牌,安全返回。");
        });

        rider.start();
        Thread.sleep(3000);
        System.out.println("用户取消订单!设置停止牌...");
        stopRequested = true;
        // 问题:如果小明此刻正在sleep(1000)里,他要等这1秒睡完才能看到stopRequested=true!
    }
}

结论:

  • 能用,但有局限!  volatile boolean 对于没有阻塞操作阻塞时间非常短且可接受延迟的线程,是一种简单有效的停止信号。
  • 不能用,当有阻塞时!  如果线程会进入sleep()wait(), 等待I/O, 等待锁等不可中断的阻塞状态volatile标志位无法唤醒它!线程会一直阻塞到自然醒(超时、数据到达、锁获取到),这可能导致响应不及时甚至线程无法停止(如果阻塞是无限期的)。
  • 推荐吗?  一般优先推荐标准的interrupt()机制,因为它能可靠地唤醒阻塞的线程,行为更一致。只有在完全确定线程不会发生不可中断阻塞,且需要更轻量级的信号时,才考虑volatile标志位。

第四幕:正确的“返航手册” - 如何优雅停止线程

总结小明安全返航的要点,就是正确停止线程的“黄金法则”:

  1. 核心:请求中断,协作退出。  使用interrupt()发出停止请求

  2. 定期检查:  在run()方法的循环条件关键执行点,使用Thread.currentThread().isInterrupted()检查中断标志。

  3. 处理阻塞:  如果代码会调用可中断的阻塞方法 (sleepwaitjoinLock.lockInterruptibly(), 某些I/O操作):

    • 将这些调用放在try块中。

    • 捕获InterruptedException

    • catch块中:

      • 立即退出循环/方法,或者
      • 恢复中断状态 (Thread.currentThread().interrupt()) 让上层代码(比如循环条件)也能看到中断,然后退出。
    • 执行必要的清理工作(关闭文件、释放锁、回滚事务等)。

  4. 避免不可中断阻塞:  尽量使用可中断的API。例如:

    • Lock.lockInterruptibly()代替synchronizedsynchronized获取锁时阻塞不可中断)。
    • InterruptibleChannel进行NIO操作。
    • socket设置超时。
  5. volatile标志位:  仅在确定线程不会进入不可中断阻塞时使用,并且要像检查中断标志一样频繁检查它。

最佳实践代码模板:

java

public class CorrectShutdown implements Runnable {
    @Override
    public void run() {
        try {
            // 循环条件检查中断
            while (!Thread.currentThread().isInterrupted()) {
                // 执行一些工作...
                doSomeWork();

                // 或者在工作单元之间检查中断
                if (Thread.currentThread().isInterrupted()) {
                    break; // 或者抛出特定异常
                }

                // 需要阻塞的地方放在try-catch里
                try {
                    Thread.sleep(1000); // 或者其他可中断阻塞方法
                    // 或者 Lock.lockInterruptibly();
                } catch (InterruptedException e) {
                    // 阻塞被中断!恢复状态并退出
                    Thread.currentThread().interrupt(); // 很重要!恢复中断标志
                    break; // 退出循环
                }
            }
        } finally {
            // 无论如何,确保清理资源!(释放锁、关闭文件、数据库连接等)
            performCleanup();
        }
    }

    private void doSomeWork() { /* ... */ }
    private void performCleanup() { /* ... */ }

    public static void main(String[][] args) throws InterruptedException {
        Thread worker = new Thread(new CorrectShutdown());
        worker.start();

        // ... 运行一段时间后需要停止 ...
        worker.interrupt(); // 发送中断请求
        worker.join(); // 等待线程完全终止 (可选)
    }
}

第五幕:指挥部的智慧 - 线程池如何“停止”骑手大军

线程池 (ExecutorService) 管理着多个“骑手”(工作线程)。停止整个池子或取消特定任务更复杂。主要用两个方法:

  1. shutdown()  温和关闭

    • 指挥部命令:  “停止接收新订单(新任务)!所有已接单的骑手(已在队列中的任务和正在执行的任务)继续送完!”
    • 实现:  设置一个状态标志拒绝新任务提交。不会主动中断正在执行的线程。线程执行完当前任务后,如果发现没有新任务且池已shutdown,就会退出。
    • 后续:  通常配合awaitTermination(timeout)等待所有线程执行完毕。
  2. shutdownNow()  立即关闭(尽力而为)

    • 指挥部命令:  “停止接收新订单尝试召回所有正在路上的骑手(尝试中断所有工作线程)!丢掉所有还没派出的订单(清空任务队列)!”

    • 实现:

      • 设置状态标志拒绝新任务。
      • 清空任务队列(返回这些被丢弃的任务列表)。
      • 遍历所有工作线程,对每个线程调用interrupt()  这就是核心!线程池向池中管理的每一个工作线程发送了interrupt()信号。
      • 结果:  工作线程能否停下,完全取决于它们是否遵循了协作式中断的规则(检查中断标志、正确处理InterruptedException)。如果线程不响应中断,它就会一直运行下去直到任务完成!shutdownNow() 不能保证所有线程立即停止,它只是尽力发出中断请求。
    • 后续:  同样配合awaitTermination

java

import java.util.concurrent.*;

public class ThreadPoolShutdown {

    public static void main(String[][] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 提交一些任务
        for (int i = 0; i < 5; i++) {
            pool.submit(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 正在配送...");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        System.out.println(Thread.currentThread().getName() + " 被中断!停止配送。");
                        Thread.currentThread().interrupt(); // 恢复中断状态
                        break;
                    }
                }
                System.out.println(Thread.currentThread().getName() + " 安全返回站点。");
            });
        }

        Thread.sleep(3000);
        System.out.println("指挥部:紧急情况!shutdownNow!");

        List<Runnable> cancelledTasks = pool.shutdownNow(); // 关键:调用每个线程的interrupt()并返回未执行任务
        System.out.println("被取消的任务数: " + cancelledTasks.size());

        // 等待所有线程真正结束 (即使被中断,也需要时间执行finally/清理)
        if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
            System.err.println("有些骑手还没回来!可能需要更强硬措施(但Java没有),或者检查代码是否忽略了中断!");
        }
    }
}
// 输出可能类似于:
// pool-1-thread-1 正在配送...
// pool-1-thread-2 正在配送...
// ... (几秒后)
// 指挥部:紧急情况!shutdownNow!
// pool-1-thread-1 被中断!停止配送。
// pool-1-thread-1 安全返回站点。
// pool-1-thread-2 被中断!停止配送。
// pool-1-thread-2 安全返回站点。
// 被取消的任务数: 3 (假设有3个还在队列里)

线程池停止关键点:

  • shutdownNow()的核心就是调用工作线程的interrupt()
  • 线程池本身不“杀”线程,它依赖于工作线程对中断的响应能力
  • 设计提交给线程池的任务 (Runnable/Callable) 时,必须使其可中断!  这是优雅停止线程池的基础。任务内部要像之前的小明一样,检查中断或处理InterruptedException
  • shutdown()更温和,适用于需要完成所有已提交任务的场景。

大结局:

  • stop()是魔鬼封印!  绝对不要用,会引发数据灾难和死锁。
  • interrupt()是协作信号!  它设置标志或唤醒阻塞线程,请求线程停止。线程必须配合检查标志或处理中断异常。
  • volatile标志有局限!  只在无阻塞或可接受延迟时有效;阻塞时无效。优先选interrupt()
  • 正确停止靠协作!  循环检查isInterrupted(),妥善处理InterruptedException(恢复标志或退出),在finally中清理资源。
  • 线程池停靠shutdown/shutdownNow  shutdownNow()底层调interrupt()提交给池的任务必须是可中断的!  shutdown温和停止接收新任务,shutdownNow激进尝试中断所有线程并清空队列。

记住,线程不是奴隶,不能粗暴对待。像对待负责任的外卖骑手小明一样,用interrupt()发出文明的“返航请求”,并设计好让它们能安全、优雅地完成手头工作或及时响应指令撤离!祝你的多线程之旅一路平安!