线程停止、中断最佳实践

如何正确停止线程

1. 讲解原理

原理介绍:使用interrupt来通知,而不是强制。 Java中停止线程的原则是什么?

在Java中,最好的停止线程的方式是使用中断interrupt, 但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。

任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束,或者让它们自行停止。然而,有时候我们希望提前结束任务或线程或许是因为用户取消了操作,或者服务需要被快速关闭,或者是运行超时或出错了。

要使任务和线程能安全、快速、可靠地停止下来并不是一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断(Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作的方式当需要停止时,它们首先会清除当前正在执行的工作然后再结束。这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作

生命周期结束(End- of-Lifecycle)的问题会使任务、服务以及程序的设计和实现等过程变得复杂,而这个在程序设计中非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运的软件之间的最主要区别就是行为良好的软件能很完善地处理失败、关闭和取消等过程。

本章将给出各种实现取消和中断的机制,以及如何编写任务和服务,使它们能对取消请求做出响应

2. 最佳实践:如何正确停止线程

2.1 线程通常会在什么情况下停止

1、run()方法运行完毕
2、 有异常出现,并且线程中没有捕获。
线程停止以后,所占用的资源会被jvm回收。

2.2 正确的停止方法:interrupt

2.2.1 通常情况下如何停止

package stopthreads;

/**
 * 描述: run()方法内没有sleep()和wait()方法时,停止线程。
 */

public class RightStopThreadWithoutSleep implements Runnable{

    public static void main(String[] args) {
        Thread thread = new Thread(new RightStopThreadWithoutSleep());
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();

    }
    @Override
    public void run() {
        int num = 0;
        while (num <= Integer.MAX_VALUE / 2){
            if (!Thread.currentThread().isInterrupted() && num % 10000 == 0) {
                System.out.println(num + "是10000的倍数");
            }
            num++;
        }
        System.out.println("任务结束了。");
    }
}

复制代码

注意: thread.interrupt();无法强制的中断线程,必须有要被中断的线程的配合。
即:需要在子线程中加上如下代码:!Thread.currentThread().isInterrupted()

2.2.2 线程可能被阻塞如何停止

package stopthreads;

import static java.lang.Thread.*;

public class RightStopThreadWithSleep {
    
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            while (num <= 300){
                if (num % 100 == 0 && !currentThread().isInterrupted()){
                    System.out.println(num + "是100的整数倍");
                }
                num++;
            }
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }

}

复制代码

结果如下:

2.2.3 如果线程在每次迭代后都阻塞

package stopthreads;

import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;

/**
 * 描述:如果在执行过程中,每次循环都会调用sleep()或wait()等方法,那么...
 */
public class rightStopThreadWithSleepEveryLoop {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;
            try {
                while (num <= 10000){
                    if (num % 100 == 0 && !currentThread().isInterrupted()){
                        System.out.println(num + "是100的整数倍");
                    }
                    num++;
                    sleep(10);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }

}

复制代码

while内的try-catch问题:

package stopthreads;

import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;

public class CantInterrupt {

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int num = 0;

                while (num <= 10000){
                    if (num % 100 == 0 && !currentThread().isInterrupted()){
                        System.out.println(num + "是100的整数倍");
                    }
                    num++;
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }

}

复制代码

改变try-catch位置,结果完全不一样:

注意: 即便添加上: && !currentThread().isInterrupted()依然没有效果!
原因: Thread类在设计时,sleep()调用中断后,interrupt标记物会自动清除!

2.3 实际开发中的两种最佳实践

2.3.1 最佳实践一:优先选择:传递中断(方法的签名上抛出异常)

我们先加一个小插曲: 错误地处理异常, 在被调用的方法中,直接把InterruptException catch掉,这样做相当于在低层的方法中就把异常给吞了,导致上层的调用无法感知到有异常。
正确做法应该是:抛出异常, 而异常的真正处理,应该叫个调用它的那个函数。
错误代码如下:

package stopthreads;

/**
 * 描述:  catch了InterruptionException之后的优先选择:在方法签名中抛出异常。
 * 那么,在run()中就会强制try-catch。
 */
public class RightWayStopThreadInProduction implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction());
        thread.start();
        thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while(true){
            System.out.println("go");
            throwInMethod();
        }
    }

    private void throwInMethod() {

        /**
         * 错误做法:这样做相当于就把异常给吞了,导致上层的调用无法感知到有异常
         * 正确做法应该是,抛出异常,而异常的真正处理,应该叫个调用它的那个函数。
         */
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

复制代码

错误处理异常导致线程无法停止:


正确做法:抛出异常,而异常的真正处理,应该叫个调用它的那个函数。
低层方法,抛出异常,调用者,就只有Surround with try/catch了。

正确代码如下:

package stopthreads;

import static java.lang.Thread.sleep;

/**
 * 描述:  catch了InterruptionException之后的优先选择:在方法签名中抛出异常。
 * 那么,在run()中就会强制try-catch。
 */
public class RightWayStopThreadInProduction implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction());
        thread.start();
        sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        while(true){
            System.out.println("go");
            throwInMethod();
        }
    }

    private void throwInMethod() throws InterruptedException {

        /**
         * 错误做法:这样做相当于就把异常给吞了,导致上层的调用无法感知到有异常
         * 正确做法应该是,抛出异常,而异常的真正处理,应该叫个调用它的那个函数。
         */
       /* try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

       sleep(2000);

    }
}

复制代码

总结:

2.3.2 最佳实践二:不想或无法传递:恢复中断(再次人为的手动把中断恢复)

在低层方法中可以try-catch,但是一定要添加 Thread.currentThread().interrupt();

package stopthreads;

import static java.lang.Thread.sleep;

public class RightWayStopThreadInProduction2 implements Runnable{

    @Override
    public void run() {

        while(true){
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("线程中断");
                break;
            }
            reInterrupt();
        }

    }

    private void reInterrupt() {

        try {
            sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProduction2());
        thread.start();
        sleep(1000);
        thread.interrupt();
    }
}

复制代码

结果:

总结:如果不能抛出中断,要怎么做?

如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws InterruptedException), 那么应该选择在catch子句中调用Thread.currentThread() interrupt()来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。具体代码见上,在这里,线程在sleep期间被中断,并且由catch捕获到该中断,并重新设置了中断状态,以便于可以在下一个循环的时候检测到中断状态,正常退出。

不应屏蔽中断

2.4 正确停止带来的好处

3. 停止现成的错误方法

4. 重要函数源码解析

5. 常见面试问题

猜你喜欢

转载自juejin.im/post/5d6c8e23f265da03c23eedb8
今日推荐