目录
一般我们在开发中想要启动线程需要调用 Thread 类的 start() 方法,并在 run() 方法中定义需要执行的任务就好了。
那么停止就不是那么好玩了,下面就总结瞎怎么停止线程。
需求:
通常情况下,我们不会手动停止一个线程,而是允许线程运行到结束,然后让它自然停止。
但是依然会有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等。
在这种情况下,即将停止的线程在很多业务场景下仍然很有价值。尤其是我们想写一个健壮性很好,能够安全应对各种场景的程序时,正确停止线程就显得格外重要。
但是Java 并没有提供简单易用,能够直接安全停止线程的能力。
所以搞多线程开发,对我们程序员来说是考验技术得时候了。
怎么停止呢?
先说结论:对于 Java 而言,最正确的停止线程的方式是使用 interrupt。
再说interrrupt:
interrupt 仅仅起到通知被停止线程的作用。
而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
这个时候又有问题了。
为什么 Java 不提供强制停止线程的能力呢?
因为Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。
比如:
线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。
如何用 interrupt 停止线程呢?
源码:
while (!Thread.currentThread().isInterrupted() && more work to do) {
do more work
}
可以看到在 while 循环体判断语句中,首先通过 Thread.currentThread().isInterrupt() 判断线程是否被中断,随后检查是否还有工作要做。
&& 逻辑表示只有当两个判断条件同时满足的情况下,才会去执行下面的工作。
具体例子:
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(5);
thread.interrupt();
}
}
- 首先判断线程是否被中断,然后判断 count 值是否小于 1000。
-
这个线程的工作内容很简单,就是打印 0~999 的数字,每打印一个数字 count 值加 1,可以看到,线程会在每次循环开始之前,检查是否被中断了。
-
接下来在 main 函数中会启动该线程,然后休眠 5 毫秒后立刻中断线程,该线程会检测到中断信号,于是在还没打印完1000个数的时候就会停下来,这种就属于通过 interrupt 正确停止线程的情况。
sleep 期间能否感受到中断呢?
先说结论:线程还在休眠,仍然能够响应中断通知,并抛出异常。
案例:
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (!Thread.currentThread().isInterrupted() && num <= 1000) {
System.out.println(num);
num++;
Thread.sleep(1000000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5);
thread.interrupt();
}
}
代码意思是:
如果线程在执行任务期间有休眠需求,也就是每打印一个数字,就进入一次 sleep ,而此时将 Thread.sleep() 的休眠时间设置为 1000 秒钟。
主线程休眠 5 毫秒后,通知子线程中断,此时子线程仍在执行 sleep 语句,处于休眠中。
所以如果不能中断就会带来严重的问题,因为响应中断太不及时了。正因为如此,Java 设计者在设计之初就考虑到了这一点。
如果 sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.qax.tsgz.toolbox.input.domain.operation.impl.StopThread.lambda$main$0(StopThread.java:33)
at java.base/java.lang.Thread.run(Thread.java:834)
正确停止代码
1、方法签名中抛出异常
先说一个反例:
在方法中使用 try/catch 或在方法签名中声明 throws InterruptedException。
void subTas() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 在这里不处理该异常是非常不好的
}
}
如上面的代码所示,catch 语句块里代码是空的,它并没有进行任何处理。
假设线程执行到这个方法,并且正在 sleep,此时有线程发送 interrupt 通知试图中断线程,就会立即抛出异常,并清除中断信号。抛出的异常被 catch 语句块捕捉。
但是,捕捉到异常的 catch 没有进行任何处理逻辑,相当于把中断信号给隐藏了,这样做是非常不合理的。
正确方式:
方法签名中抛出异常。
void subTask2() throws InterruptedException {
Thread.sleep(1000);
}
正如代码所示,要求每一个方法的调用方有义务去处理异常。
调用方要不使用 try/catch 并在 catch 中正确处理异常,要不将异常声明到方法签名中。
如果每层逻辑都遵守规范,便可以将中断信号层层传递到顶层,最终让 run() 方法可以捕获到异常。
而对于 run() 方法而言,它本身没有抛出 checkedException 的能力,只能通过 try/catch 来处理异常。层层传递异常的逻辑保障了异常不会被遗漏,而对 run() 方法而言,就可以根据不同的业务逻辑来进行相应的处理。
2、再次中断
在 catch 语句块中调用 Thread.currentThread().interrupt() 函数。
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
因为如果线程在休眠期间被中断,那么会自动清除中断信号。如果这时手动添加中断信号,中断信号依然可以被捕捉到。
这样后续执行的方法依然可以检测到这里发生过中断,可以做出相应的处理,整个线程可以正常退出。
需要注意的是:
我们需要注意,我们在实际开发中不能盲目吞掉中断。
我们需要注意,我们在实际开发中不能盲目吞掉中断。
我们需要注意,我们在实际开发中不能盲目吞掉中断。
如果不在方法签名中声明,也不在 catch 语句块中再次恢复中断,而是在 catch 中不作处理,我们称这种行为是【屏蔽了中断请求】。
如果我们盲目地屏蔽了中断请求,会导致中断信号被完全忽略,最终导致线程无法正确停止。