线程停止、中断之最佳实践(慕课网 -> 线程八大核心+Java并发底层原理精讲 掌握企业级并发问题解决方案)

什么情况下,线程需要被停止?

线程和任务被创建和启动之后,大部分情况下都是自然运行到结束的,自然停止,但有些情况会需要用到停止线程,如:或许用户主动取消,或许服务被快速关闭,或者运行出错或超时情况下线程都需要被停止,这些情况都需要我们主动来停止线程,想让线程安全可靠停止下来并不容易,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() 来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断,正常退出。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值