文章目录
在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(),任务也会继续执行。
三、避坑指南:这些错误千万别犯!
- 📌 标志位不加volatile:线程看不到标志位修改,一直干活停不下来 —— 相当于工人没听到下班口号;
- ✔️ 吞掉InterruptedException:在catch块里只打印日志,不处理中断,导致线程不知道被中断 —— 相当于工人听到下班通知,却假装没听见,继续干活;
❌ 错误示例:
catch (InterruptedException e) {
e.printStackTrace(); // 只打印日志,没重新设置中断标志
}
✅ 正确示例:
catch (InterruptedException e) {
System.out.println("收到中断,准备停止");
Thread.currentThread().interrupt(); // 重新设置中断标志,让线程退出
}
- 🎲 线程阻塞时不处理停止:线程在Socket.read()、Lock.lock()(非可中断锁)等阻塞方法中,不会响应中断和标志位,导致无法停止 —— 相当于工人睡着了,喊不醒也听不到口号;
解决方案:用可中断的阻塞方法(比如Lock.lockInterruptibly()),或设置超时时间(比如Socket.setSoTimeout())。 - 🚀 用stop()等废弃方法:就算代码能运行,也会被面试官骂 “不专业”,还可能导致程序崩溃 —— 相当于暴力裁员,后患无穷。
四、总结: 不同场景选对 “劝退” 姿势
| 场景 | 推荐方法 | 一句话口诀 |
|---|---|---|
| 单个线程,需要响应中断 | 协作式中断(interrupt()+isInterrupted()) | 中断通知,线程自检 |
| 单个线程,循环任务,阻塞少 | 自定义volatile标志位 | 标志位开关,循环检查 |
| 线程池提交的单个任务 | Future.cancel(true) | 任务合同,按需取消 |
| 整个线程池关闭 | shutdown()+awaitTermination() | 优雅关门,等待收尾 |
核心口诀:线程停止不硬拽,协作式通知最安全;中断标志位优先,线程池关闭按流程;废弃方法千万别碰,资源收尾要做好!
用热梗收尾:停止线程就像 “劝朋友别熬夜”—— 你只能提醒(发通知),朋友得自己放下手机(线程退出),硬抢手机(强制停止)只会伤感情(程序崩溃)!


1501

被折叠的 条评论
为什么被折叠?



