【008】Java程序中正确地停止一个线程?

在Java程序中正确停止一个线程的方法:

1、使用中断: 调用线程的interrupt()方法来设置线程的中断状态;线程需要定期检查自身的中断状态,并相应地响应中断。

2、使用标志位: 设置一个需要线程检查的标志位,线程周期性地检查该标志,以决定是否停止运行。

3、避免使用stop()方法: 不建议使用Thread类的stop()方法来停止线程,因为它是不安全的。

Java 线程停止:别硬拽!优雅 “劝退” 线程的 4 种正确姿势

如果把 Java 线程比作 “正在干活的工人”,停止线程就不是 “直接把工人踹走”(暴力停止),而是 “跟工人商量:活先停一下,收拾好工具再下班”(协作式停止)。Java 里强行停止线程的方法(比如stop())早就被废弃,就像 “暴力裁员” 一样容易出乱子 —— 数据丢失、资源泄露、程序崩溃都可能发生。
今天就用 “工人干活” 的比喻,讲清楚 4 种正确停止线程的姿势,每个都配代码 + 场景,保证你看完就会用,还能避开所有坑!

一、核心原则: 线程停止必须 “协作式”,拒绝 “强制式”

Java 线程设计的核心思想是:线程只能自己停止,外部只能 “发通知”,不能 “强按头”。

就像工人干活,你不能直接把他手里的工具抢了(强制停止),只能喊一声 “下班了”(发通知),工人听到后,把手里的活收尾(保存数据、释放资源),然后自己下班(线程退出)。

【反面教材】:3 个被废弃的 “暴力方法”(千万别用!)
【反面教材】:3 个被废弃的 “暴力方法”(千万别用!)
【反面教材】:3 个被废弃的 “暴力方法”(千万别用!)

废弃方法比喻(工人干活)为什么危险?
stop()直接把工人踹走,工具扔一地线程执行到一半被强制终止,可能导致数据不一致(比如写文件写到一半停了)、资源泄露(锁没释放)
suspend()把工人捆起来,不让干活也不让走线程被挂起后不会释放锁,容易导致死锁(其他线程等着这个锁,永远拿不到)
resume()把捆着的工人解开,让他继续干活配合suspend()使用,容易出现 “工人被捆着没人解”(线程挂起后没调用resume()),导致线程永久阻塞

这三个方法就像 “职场 PUA + 暴力裁员”,不仅不优雅,还会留下一堆烂摊子,Java 官方早就标为@Deprecated,生产环境敢用,出问题没人救你!

二、正确姿势:4 种优雅 “劝退” 线程的方法

2.1 姿势 1:协作式中断(推荐!)—— 给工人 “拍肩膀提醒”

这是 Java 官方推荐的核心方法,核心是interrupt()(发中断通知)+ isInterrupted()(工人检查通知)

  • interrupt():不是 “停止线程”,而是给线程设置一个 “中断标志位”(相当于喊 “下班了”);
  • isInterrupted():线程自己检查 “中断标志位”(相当于工人时不时看一眼有没有下班通知);
  • 线程收到通知后,自己决定什么时候停止,还能收拾好资源(保存数据、释放锁)。

生活场景:工人正在下载文件,收到 “停止下载” 通知后,先保存已下载的部分,再停止。

🎲 示例代码:

package cn.tcmeta.threads;

import static java.lang.Thread.sleep;

/**
 * @author: laoren
 * @description: 停止线程: interrupt()(发中断通知)+ isInterrupted()(工人检查通知)
 * @version: 1.0.0
 */
public class InterruptSample {
    static void main() throws InterruptedException {
        // 创建“下载工人”线程
        Thread downloadThread = new Thread(() -> {
            System.out.println("工人:开始下载文件...");
            try {
                // 模拟下载:循环执行,每100ms检查一次是否有中断通知
                for (int i = 0; i < 10; i++) {
                    // 关键:检查中断标志位(工人看有没有下班通知)
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("工人:收到停止通知,保存已下载数据...");
                        // 收拾资源后,主动退出线程
                        return;
                    }
                    System.out.println("工人:下载进度" + (i + 1) * 10 + "%");
                    sleep(100); // 模拟下载耗时(sleep时会响应中断)
                }
                System.out.println("工人:文件下载完成!");
            } catch (InterruptedException e) {
                // 关键:sleep()被中断时,会抛出异常并清除中断标志位
                System.out.println("工人:下载时收到停止通知,保存数据...");
                // (可选)如果需要让上层知道,可重新设置中断标志位
                Thread.currentThread().interrupt();
            }
        }, "下载线程");


        // 启动工人干活
        downloadThread.start();
        // 主线程让工人干300ms后,发停止通知
        sleep(300);
        System.out.println("主线程:通知工人停止下载!");
        downloadThread.interrupt(); // 发中断通知(设置标志位)
    }
}

运行结果(优雅停止,无数据丢失):
在这里插入图片描述
【关键注意点】:

  • sleep()、wait()、join()等方法会 “响应中断”
    • 如果线程在这些方法中阻塞,调用interrupt()会让线程抛出InterruptedException,并清除中断标志位(所以异常里如果需要继续中断,要重新调用interrupt());
  • 线程必须主动检查isInterrupted()(或Thread.interrupted()),否则就算收到中断通知也不会停止(比如工人一直干活不看通知);
  • Thread.interrupted()和isInterrupted()的区别:前者会清除中断标志位(看一眼通知后就删掉),后者不会(一直保留通知),推荐用isInterrupted()。

2.2 自定义标志位 —— 给工人 “喊口号通知”

如果觉得中断机制太 “绕”,可以自己定义一个 “停止标志位”(比如volatile boolean stopFlag),线程循环检查这个标志位,外部修改标志位就能 “劝退” 线程。

相当于给工人喊口号:“下班了!”,工人每隔一会儿就听一听,听到了就收拾东西下班。

生活场景:工人正在打印文件,外部设置stopFlag=true,工人检查到后停止打印。

package cn.tcmeta.threads;

import static java.lang.Thread.sleep;

/**
 * @author: laoren
 * @description: 自定义标志位
 * @version: 1.0.0
 */
public class FlagStopSample {
    // 停止标志位:必须加volatile!保证线程间可见性(工人能及时听到口号)
    private static volatile boolean stopFlag = false;

    static void main() throws InterruptedException {
        Thread printThread = new Thread(() -> {
            System.out.println("工人:开始打印文件...");
            int count = 1;
            // 循环检查标志位:stopFlag为true就停止
            while (!stopFlag) {
                System.out.println("工人:打印第" + count + "页");
                count++;
                try {
                    sleep(100); // 模拟打印耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("工人:收到停止通知,关闭打印机,下班!");
        }, "打印线程");

        printThread.start();
        // 主线程让工人打印300ms后,喊“下班”
        sleep(300);
        System.out.println("主线程:喊工人下班!");
        stopFlag = true; // 修改标志位,通知线程停止
    }
}

在这里插入图片描述
【关键注意事项】

  • 标志位必须加volatile!否则线程可能 “看不到” 标志位的修改(JVM 优化导致缓存不可见),相当于工人没听到口号,一直干活;
  • 局限性:如果线程处于阻塞状态(比如sleep()、wait()、Socket.read()),不会检查标志位,导致 “喊了下- - 班但工人没反应”。比如工人睡着了(sleep(1000)),你喊下班,他要等睡醒了才会检查标志位;
  • 适用场景:线程执行的是 “循环任务”(比如打印、扫描),且阻塞时间短,能及时检查标志位。

2.3 姿势 3:Future.cancel ()—— 给 “临时工”(线程池任务)发 “解雇通知”

如果线程是通过线程池提交的任务(比如ExecutorService.submit()),可以用Future的cancel()方法停止任务,相当于给 “临时工” 发解雇通知。

Future.cancel(boolean mayInterruptIfRunning)有个参数:

  • true:如果任务正在执行,就用 “中断” 的方式停止(需要任务响应中断);
  • false:只取消 “还没开始执行” 的任务,正在执行的任务让它继续做完。

生活场景:线程池提交了一个 “数据分析” 的临时工任务,发现数据错了,需要停止任务。

package cn.tcmeta.threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static java.lang.Thread.sleep;

/**
 * @author: laoren
 * @description: Future.cancel ()—— 给 “临时工”(线程池任务)发 “解雇通知”
 * @version: 1.0.0
 */
public class FutureCancelSample {

    static void main() throws InterruptedException {
        // 创建线程池(3个工人)
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 提交任务,返回Future(相当于“临时工合同”)
        Future<String> future = executor.submit(() -> {
            System.out.println("临时工:开始数据分析...");
            try {
                // 模拟数据分析:循环执行,响应中断
                for (int i = 0; i < 10; i++) {
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("临时工:收到解雇通知,清理数据...");
                        return "任务被取消";
                    }
                    System.out.println("临时工:分析进度" + (i + 1) * 10 + "%");
                    sleep(100);
                }
                return "数据分析完成";
            } catch (InterruptedException e) {
                System.out.println("临时工:分析时被解雇,清理数据...");
                return "任务被取消";
            }
        });

        // 主线程等300ms后,取消任务
        sleep(300);
        System.out.println("主线程:数据错了,解雇临时工!");
        // cancel(true):中断正在执行的任务
        boolean isCancelled = future.cancel(true);
        System.out.println("任务是否取消成功?" + isCancelled);

        // 关闭线程池
        executor.shutdown();
    }
}

在这里插入图片描述
【关键注意事项】

  • cancel(true)只有在任务 “响应中断” 时才有效 —— 如果任务里没有检查isInterrupted(),也没有调用sleep()等可中断方法,就算调用cancel(true)也停不了;
  • 如果任务已经执行完,cancel()会返回false(取消失败);
  • 适用场景:线程池提交的异步任务(比如CompletableFuture也支持类似的取消机制),需要灵活控制单个任务的停止。

2.4 线程池优雅关闭 ——“工厂下班,工人有序收尾”

如果是线程池(比如ThreadPoolExecutor),停止线程池不是 “直接炸工厂”,而是 “关闭工厂大门,不让新任务进来,等里面的工人把活干完再锁门”,核心方法是shutdown()、shutdownNow()、awaitTermination()。

线程池方法比喻(工厂下班)核心逻辑
shutdown()关工厂大门,不让新订单进来,工人干完手里的活再下班优雅关闭,不中断正在执行的任务,拒绝新任务
shutdownNow()关大门 + 喊所有工人停工,收拾工具下班尝试中断所有正在执行的任务,返回未执行的任务列表,风险比shutdown()
awaitTermination(long timeout, TimeUnit unit)站在工厂门口等,超时没下班就走阻塞等待线程池关闭完成,返回true(按时关闭)/false(超时未关闭),用于确认关闭结果

生活场景:工厂(线程池)有 3 个工人,正在处理订单,到下班时间了,优雅关闭工厂。

package cn.tcmeta.threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author: laoren
 * @description: 优雅关闭线程池
 * @version: 1.0.0
 */
public class ThreadPoolShutdownSample {
    static void main() throws InterruptedException {
        // 创建线程池(3个工人)
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 提交5个订单任务
        for (int i = 1; i <= 5; i++) {
            int orderId = i;
            executor.submit(() -> {
                System.out.println("工人:处理订单" + orderId);
                try {
                    Thread.sleep(500); // 模拟处理耗时
                } catch (InterruptedException e) {
                    System.out.println("工人:订单" + orderId + "被中断,清理资源...");
                    return;
                }
                System.out.println("工人:订单" + orderId + "处理完成");
            });
        }

        // 1. 关闭线程池:不让新任务进来(shutdown())
        executor.shutdown();
        System.out.println("工厂:大门关闭,不再接新订单!");

        // 2. 等待3秒,看工人是否干完活(awaitTermination())
        boolean isShutdown = executor.awaitTermination(3, TimeUnit.SECONDS);
        if (isShutdown) {
            System.out.println("工厂:所有工人下班,工厂关闭!");
        } else {
            System.out.println("工厂:超时了,强制让工人停工!");
            // 3. 超时未关闭,尝试中断所有任务(shutdownNow())
            executor.shutdownNow();
        }
    }
}

在这里插入图片描述
【关键注意点】:

  • 永远不要用executor.shutdownNow()直接关闭线程池 —— 它会中断所有正在执行的任务,容易导致数据丢失,只在 “超时未关闭” 时作为兜底;
  • 关闭线程池的正确流程:shutdown() → awaitTermination() → (超时)shutdownNow();
  • 线程池中的任务如果需要响应shutdownNow(),必须支持中断(检查isInterrupted()),否则就算调用shutdownNow(),任务也会继续执行。

三、避坑指南:这些错误千万别犯!

  1. 📌 标志位不加volatile:线程看不到标志位修改,一直干活停不下来 —— 相当于工人没听到下班口号;
  2. ✔️ 吞掉InterruptedException:在catch块里只打印日志,不处理中断,导致线程不知道被中断 —— 相当于工人听到下班通知,却假装没听见,继续干活;
    ❌ 错误示例:
catch (InterruptedException e) {
    e.printStackTrace(); // 只打印日志,没重新设置中断标志
}
	✅ 正确示例:
catch (InterruptedException e) {
    System.out.println("收到中断,准备停止");
    Thread.currentThread().interrupt(); // 重新设置中断标志,让线程退出
}
  1. 🎲 线程阻塞时不处理停止:线程在Socket.read()、Lock.lock()(非可中断锁)等阻塞方法中,不会响应中断和标志位,导致无法停止 —— 相当于工人睡着了,喊不醒也听不到口号;
    解决方案:用可中断的阻塞方法(比如Lock.lockInterruptibly()),或设置超时时间(比如Socket.setSoTimeout())。
  2. 🚀 用stop()等废弃方法:就算代码能运行,也会被面试官骂 “不专业”,还可能导致程序崩溃 —— 相当于暴力裁员,后患无穷。

四、总结: 不同场景选对 “劝退” 姿势

场景推荐方法一句话口诀
单个线程,需要响应中断协作式中断(interrupt()+isInterrupted()中断通知,线程自检
单个线程,循环任务,阻塞少自定义volatile标志位标志位开关,循环检查
线程池提交的单个任务Future.cancel(true)任务合同,按需取消
整个线程池关闭shutdown()+awaitTermination()优雅关门,等待收尾

核心口诀:线程停止不硬拽,协作式通知最安全;中断标志位优先,线程池关闭按流程;废弃方法千万别碰,资源收尾要做好!
用热梗收尾:停止线程就像 “劝朋友别熬夜”—— 你只能提醒(发通知),朋友得自己放下手机(线程退出),硬抢手机(强制停止)只会伤感情(程序崩溃)!
请添加图片描述
请添加图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值