一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
简介
我们可以调用线程对象的 start()
方法启动线程,而停止线程则有些复杂。
通常情况下,我们不会手动停止一个线程,而是允许线程运行到整个进程结束,然后让它自然停止。但是对于某些特殊情况,需要提前停止线程,比如用户突然关闭程序、程序运行出错等。此时即将停止的线程在很多业务场景下仍然很有价值,但是 Java
并没有提供简单易用,能够直接安全停止线程的能力。
对于 Java
而言,最正确的停止线程的方式是使用 interrupt()
,但它仅仅起到通知被停止线程的作用。对于被停止的线程而言,它拥有完全的自主权,既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
为什么 Java
不提供强制停止线程的能力?
Java
希望程序间能够相互通知、相互协作地管理线程- 因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题
- 比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据错乱,不管是中断命令发起者,还是接收者都不希望数据出现问题
停止线程的错误方式
以下三种停止线程的方式被标记为已过时 @Deprecated
:
stop()
:停止线程运行。stop()
会直接把线程停止,虽然它会释放锁,但是会导致任务戛然而止,没有给线程足够的时间处理收尾工作,可能会出现数据完整性等问题。suspend()
/resume()
:挂起线程运行 / 恢复线程运行。线程调用suspend()
后,不会释放锁。在该线程被resume()
之前,这把锁不会被释放。这样就可能导致死锁问题。- 借助
volatile
标记位
为什么借助 volatile
标记位停止线程是错误的?
在一些情况下,volatile
标记位可以正常停止线程,如下:
public class VolatileStopThread implements Runnable {
private volatile boolean canceled = false;
@Override
public void run() {
int num = 0;
try {
while (!canceled && num <= 1000000) {
if (num % 10 == 0) {
System.out.println(num + "是10的倍数");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
VolatileStopThread r = new VolatileStopThread();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(3000);
r.canceled = true;
}
}
复制代码
但是,如果被停止的线程在执行过程中被阻塞了(比如往已经放满的阻塞队列中添加元素),那么它将无法检测到 volatile
标记位已经被设置为 false
,从而无法跳出循环。所以不能借助 volatile
标记位来停止线程。
interrupt() 方法与中断标记位
调用线程的 interrupt()
方法,可以停止线程,在这里分为两种情况:
- 调用运行中线程的
interrupt()
方法,会设置中断标记位。 - 调用阻塞中线程的
interrupt()
方法,比如调用了sleep()
、wait()
、join()
方法的线程,会抛出InterruptedException
异常,同时清除中断标记,也就是将其设置为false
。
访问中断标记位有哪些方法?
isInterrupted()
:该方法不会影响中断标记。- 静态方法
Thread.interrupted()
:返回中断标记后,会将其设置为false
。也就是说,该方法可以用于清空中断标记位。
正确停止线程
方式一:在方法签名中声明抛出异常,在 run()
方法中使用 try/catch
捕获异常。
- 用
throws InterruptedException
标记方法,不捕获异常,以便该异常可以传递到顶层的run()
方法 - 由于
run()
方法无法抛出受检异常,所以只能使用try/catch
捕获异常,这样的好处在于,顶层方法必须处理该异常
方式二:在 catch
块中再次中断线程。
- 在
catch
块中调用Thread.currentThread().interrupt()
函数 - 因为如果线程在休眠期间被中断,那么会自动清除中断信号
- 这时如果手动添加中断信号,中断信号仍然可以被捕捉到
如果盲目地忽略中断请求,也就是屏蔽中断信号,可能会导致线程无法被正确停止。
两阶段终止模式
上面的方式二其实就是两阶段终止模式。
两阶段终止模式 Two-Phase Termination Pattern
是一种多线程设计模式,用于在线程 t1
中优雅地停止另一个线程 t2
,“优雅”指的是给被停止的线程做收尾工作的机会。
两阶段终止分为两种中断情况:
- 运行时中断,打断标记已经为
true
,只需要检测即可 - 睡眠时中断,需要捕捉异常,以及重新设置打断标记为
true
团子注:使用
interrupt()
实现两阶段终止。关键点在于对于sleep
、join
或者wait
等处于阻塞状态的线程,interrupt()
只会抛出InterruptedException
,而不会设置打断标记,所以只要在异常处理内容中手动再次interrupt()
重新设置打断标记即可。
流程图如下:
示例代码如下:
class TPTInterrupt {
private Thread thread;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
current.interrupt();
}
// 执行监控操作
}
}, "监控线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
复制代码
调用:
TPTInterrupt t = new TPTInterrupt();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop()
复制代码
结果:
11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事
复制代码
参考资料
- 拉勾:
Java
并发编程78
讲第2
讲 - [[202112052113 Java 多线程:interrupt 方法]]
- [[202112052155 Java 多线程设计模式:两阶段终止]]
- [[202112052217 Java 多线程:不推荐使用的过时方法]]