什么情况下,线程需要被停止?
线程和任务被创建和启动之后,大部分情况下都是自然运行到结束的,自然停止,但有些情况会需要用到停止线程,如:或许用户主动取消,或许服务被快速关闭,或者运行出错或超时情况下线程都需要被停止,这些情况都需要我们主动来停止线程,想让线程安全可靠停止下来并不容易,Java语言没有一种机制来安全正确地停止线程,但是它提供了interrupt,这是一种协作机制。
如何正确停止线程?
原理介绍:使用interrupt来通知,而不是强制。
通俗理解:就是用一个线程来通知另一个线程让它停止工作,在Java中,我们如果想停止一个线程,能做的最多能做的就是告诉一个线程,你该中断了,而被中断的线程本身拥有决定权,它不但能决定何时去响应这个中断,何时去停止,还拥有最高决定权,就是停不停止,也就是说,我们想中断一个线程,但那个线程自身并不想被中断,那我们对此无能为力,而不是强制的含义,我们作为想停止线程的一方,根本没有能力做到强行停止。但是如果说,在编码中,小组或大部门都遵守良好的规范的话,那自然我们都把编码处理成可以响应interrupt中断来停止的,但这仅仅是一个规范,不是一种强制。
我们是程序的控制者,凭什么我们没有控制线程停止的权利?
其实大多数时候我们想停止一个线程,都会至少让它运行到结束,比如说,即便我们关机的时候,也会在关机的时候做很多的收尾工作,结束一些进程、线程,保存一些状态,那么其他线程也是一样的,由于我们想中断的是其他线程,有可能不是我们来写的,对这个线程执行的业务逻辑根本就不熟悉,我们想让它停止,其实是希望它完成一系列的保存工作,交接工作,再停止,而不是让它立刻停止,让它陷入一种混乱的状态。所以,被停止的那个线程,对自己的业务逻辑是最熟悉的,而发出停止信号的线程,它对别人的业务逻辑很可能是不了解的,所以java语言设计的时候就把线程停止的权力和步骤交给了被停止线程本身,这就是停止线程的核心,而不是强制停止。
停止线程最佳实践
1、通常线程会在什么情况下停止,普通情况
正常创建和启动一个线程
/**
* 描述:run方法没有 sleep 和 wait方法
* */
public class RightWayStopThreadWithOutSleep implements Runnable{
@Override
public void run() {
int num = 0;
while (num <= Integer.MAX_VALUE/2){
if (num%10000 == 0){
System.out.println(num+"是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
public static void main(String[] args) {
new Thread(new RightWayStopThreadWithOutSleep()).start();
}
}
打印结果1
初步尝试使用interrupt()方法中断
package threadcoreknowledge.stopthreads;
/**
* 描述:run方法没有 sleep 和 wait方法
* */
public class RightWayStopThreadWithOutSleep implements Runnable{
@Override
public void run() {
int num = 0;
while (num <= Integer.MAX_VALUE/2){
if (num%10000 == 0){
System.out.println(num+"是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithOutSleep());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果2
你会发现,没有任何效果,我们想把这个线程中断,但它似乎根本就没理会我们,这个线程想不想停止,取决于它本身,所以我们需要对它做响应工作的编写。这样一来,才能响应我们刚发出来的中断信号。
增加中断响应!Thread.currentThread().isInterrupted()
。
当线程没有被中断,即Thread.currentThread().isInterrupted()
等于false时,!Thread.currentThread().isInterrupted()
等于true,线程继续执行,否则线程被停止。
package threadcoreknowledge.stopthreads;
/**
* 描述:run方法没有 sleep 和 wait方法
* */
public class RightWayStopThreadWithOutSleep implements Runnable{
@Override
public void run() {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE/2){
if (num%10000 == 0){
System.out.println(num+"是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithOutSleep());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果3
看最后的数值与之前相比,明显线程执行 1秒左右 被停止了,interrupt()方法中断有效。
2、当停止线程遇到线程阻塞,怎么办?
代码思路:开启一个线程执行任务:num从0循环递增+1到300,打印出该过程中是100倍数的数。执行完任务后让该线程睡眠1s,在线程阻塞时发出停止线程请求。
示例:
package threadcoreknowledge.stopthreads;
/**
* 描述:带sleep的中断线程方法
* */
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
try {
while (!Thread.currentThread().isInterrupted() && num <= 300){
if (num % 100 == 0){
System.out.println(num+"是100的倍数");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
打印结果:
发现报异常,回顾一下,当我们调用sleep()方法,要求出try-catch这个异常,然后打印的结果也是catch到了,java.lang.InterruptedException: sleep interrupt
,为什么会报异常呢,是这样的,当线程在休眠状态下,如果收到这个中断信号,线程便会响应这个中断,而响应这个中断的方式非常特殊,就是抛出这个异常,于是我们就打印了这个异常sleep interrupted
,就是在sleep过程中被打断了。当我们程序带有sleep,或者能让线程阻塞的方法,我们所需要进行中断的时候,需要注意的。因为我们用到sleep等让线程阻塞方法时,必然会要求我们处理InterruptedException这个异常,我们选择catch这种方法处理之后,就可以做到当线程进入阻塞过程中,依然可以响应这个中断。
3、线程每次迭代都阻塞
示例:
package threadcoreknowledge.stopthreads;
/**
* 描述:带sleep的中断线程方法
* */
public class RightWayStopThreadWithSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
try {
while (num <= 10000){
if (num % 100 == 0){
System.out.println(num+"是100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
当线程每次迭代都阻塞时,就不再需要Thread.currentThread().isInterrupted()
判断线程是否被中断了,由于每次循环都会进入到sleep过程中,而每次循环过程中又不会特别长,所以其实大部分时间是消耗在Thread.sleep(10);
里面的,而在休眠的过程中如果接收到interrupt,自然会抛出异常
,不要在代码中加入Thread.currentThread().isInterrupted()
判断检查是否已中断。
4、如果While里面放try / catch,会导致中断失效
示例:
package threadcoreknowledge.stopthreads;
/**
* 描述:如果While里面放try / catch,会导致中断失效
* */
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
while (num <= 10000){
if (num % 100 == 0){
System.out.println(num+"是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
惊奇地发现,到第5秒时,明明已经catch了InterruptedException
异常,但还在线程并没有就此停止,反而继续打印,这是为什么呢?因为抛出异常后被catch住了,然后因为不满足循环跳出的条件,所以自然继续执行while循环。
那是不是加上在while()里加上&& !Thread.currentThread().isInterrupted()
,在线程中断后,下一次循环开始时判断一下线程是否已被中断即可?!来试一试吧~。
修改代码:
package threadcoreknowledge.stopthreads;
/**
* 描述:如果While里面放try / catch,会导致中断失效
* */
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
int num = 0;
//while()加一个条件:!Thread.currentThread().isInterrupted()
while (num <= 10000 && !Thread.currentThread().isInterrupted()){
if (num % 100 == 0){
System.out.println(num+"是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
打印结果:
就很神奇啊,线程依然没有停止!!!
之所以会产生这样原因是因为java在设计sleep()函数的时候,有这样一个理念,当它一旦响应中断,于是便会把interrupt这个标记位给清除,所以在上面代码while后续检查过程中检查不到任何被中断过的迹象,导致程序不能退出。如何解决呢?下面就来解决这个问题。
实际开发中两种最佳实践
一、第一个最佳实践:
通常在run()方法中不会一次性把所用业务都写在该方法中,我们可能会调用其他的方法。假设被调用的方法在某些代码环节可能需要处理异常,这是通常就是两种方法:一是try/catch
,二是在方法签名上抛出这个异常 throws xxxException
。下面先来演示一下非常不好的try/catch,说一下为什么不好。
1、举一个使用try-catch的例子:
思路是:调用一个通过try/catch处理异常的方法,方法内容是睡眠2秒,启动子线程后,主线程睡眠 1秒后发起中断请求,确保子线程是在阻塞状态下响应中断。
package threadcoreknowledge.stopthreads;
/**
* 描述: 最佳实践:catch了interruptedException之后的优先选择:在方法签名中抛出异常
* 那么在run()方法就会强制要求try/catch
* */
public class RightWayStopThreadInProd implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("go");
throwInMethod();
}
}
private void throwInMethod() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new
RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果:
try/catch为什么不好,不好在那里?
上图明显异常已经打印了,但是不好的是,在茫茫控制台信息中是很难注意到这个异常的,由于我们很难注意到它,我们就很难去处理它,所以实际上当有一个线程来打断我们时,我们没有处理好,非但没有处理好,反而把这个信息给忽略掉了,我们看似没有忽略,把内容打印出来,但在我们实践过程中,代码跑在服务器上,我们并不一定能感知到这个问题。假设throwInMethod()方法是其他小伙伴写的,我们只是负责调用的话,那遗憾的事情就发生了,别人想中断我们,但我们没有响应,对于我们来说不但没有响应,还毫不知情,因为我们是负责写run()方法的内容,throwInMethod()方法是其他小伙伴负责的,我们并不了解里面的业务逻辑,所以我们就轻轻松松简简单单地调用它,最后的责任却是在我们这里,因为其他线程想中断我们,我们却没有响应中断。在throwInMethod()方法中直接把中断给吞了,什么叫吞了呢?就是在throwInMethod()方法休眠的过程中有一个中断过来,但是它没有做什么处理,只是把它打印出来,没有做额外的处理,没有上报给调用它的方法(run方法),它要做的应该是上报给我们run方法,因为它无法做出更多中断处理,实际是由我们调用方,我们的run方法处理,去决定在调用方法这步有异常情况我们应该怎么处理,是该保存日志或其它操作等等,这是我们的责任,而编写throwInMethod()方法的小伙伴的责任绝不是把异常简单的打印出来,自己吞掉,应该上报给我们,把中断的这个信息传给我们
。
2、正确的最佳实践例子:
package threadcoreknowledge.stopthreads;
/**
* 描述: 最佳实践1:catch了interruptedException之后的优先选择:在方法签名中抛出异常
* 那么在run()方法就会强制要求try/catch
* */
public class RightWayStopThreadInProd implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
//保存日志、停止程序
System.out.println("保存日志");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new
RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
3、小结第一个最佳实践:
处理中断的最好方法是什么?
传递中断:优先选择在方法上抛出异常。
用throws InterruptedException 标记你的方法,不采用try 语句块捕获异常,以便于该异常可以传递到顶层,让run方法可以捕获这一异常,例如:
void subTask() throws InterruptedException
sleep(delay);
}
由于run方法内无法抛出checked Exception(只能用try catch),顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。
二、第二个最佳实践
我们上面说优先处理中断的方法是传递中断
,但是在有些情况我们是无法传递的,比如说,我们是作为run()方法的编写者,在run()方法中是不允许抛出异常的,或者有时候我们确实不想在这个方法上抛出异常,我们如果想就是自己处理的话,这边也给出了一种对应的方法:不想或无法传递中断:恢复中断
恢复中断总体而言就是我们在获取InterruptException的同时,应该在catch语句中再次调用Thread.currentThread().isInterrupted()
,这样就相当于自己把中断重新设置了一遍,这样一来在后续的执行中依然能检测到刚才发生的这个中断,并且有后续的逻辑继续去处理。
1、恢复中断示例:
package threadcoreknowledge.stopthreads;
/**
* 描述: 最佳实践2:在catch子语句中调用Thread
* .currentThread.interrupt()来恢复中断状态,
* 以便于在后续的执行中,依然能够检查到刚才发生的中断
* 回到刚才RightWayStopThreadInProd补上中断,让它
* 跳出
* */
public class RightWayStopThreadInProd2 implements Runnable{
@Override
public void run() {
while(true) {
if (Thread.currentThread().isInterrupted()){
System.out.println("Interrupted,程序运行结束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new
RightWayStopThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
打印结果
2、小结第二个最佳实践:
如果不能抛出中断,要怎么做?
如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws InterruptedException),那么应该选择在catch 子句中调用Thread.currentThread().interrupt() 来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断,正常退出。