3 如何正确停止线程

525 阅读7分钟

此笔记使用wolai进行梳理,欢迎来访,指出问题 www.wolai.com/mucong/8EaQ…

3.1 原理介绍


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

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

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

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

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


3.2 最佳实践


提前总结

线程什么时候停止

  • run()方法执行完毕

  • 出现异常,并且没有捕获

  • 正确的停止方式:使用interrupt()方法

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


  • run方法内没有sleep或wait方法时,停止线程

  • 使用interrupt() 停止线程

  • 使用interrupt() 时需要使用!Thread.currentThread().isInterrupted()判断是否收到中断指令

  • 使用代码演示

public class RightWayStopThreadWithoutSleep implements Runnable{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        //使用interrupt方法通知线程停止
        thread.sleep(1000);
        thread.interrupt();

    }
    @Override
    public void run() {
        int number = 0 ;
        //Thread.currentThread().isInterrupted() 
        //这个方法可以判断方法是否接收到了中断指令
        while (number <= Integer.MAX_VALUE/2 && !Thread.currentThread().isInterrupted()){
            if (number%10000 == 0){
                System.out.println(number);
            }
            number++;
        }
        System.out.println("任务结束了");
    }
}

3.2.2 阻塞的情况下停止线程


  • 当子线程线程自身阻塞时,停止线程

  • 使用interrupt() 停止线程

  • 使用interrupt() 时需要使用!Thread.currentThread().isInterrupted()判断是否收到中断指令

  • 使用sleep阻塞时,中断通知会使线程抛出响应中断通知的异常,然后停止线程

  • 使用代码演示

public class RightWayStopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int number = 0;
            try {
            while ( number <= 300 && !Thread.currentThread().isInterrupted()){
                if ( number%100 == 0){
                    System.out.println(number);
                }
                number++;
            }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("任务执行完毕");
        };
        Thread thread = new Thread(runnable);
        thread.start();
        thread.sleep(500);
        thread.interrupt();
    }
}

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


  • 如果在执行过程中,每次循环都会调用sleep或者wait方法,那么我们就不需要每次迭代都检查是否已中断

  • 使用interrupt() 停止线程

  • 使用interrupt() 时不需要使用!Thread.currentThread().isInterrupted()判断是否收到中断指令

  • 使用代码演示

public class RightWayStopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            int number = 0;
            try {
                while ( number <= 10000 ){
                    if ( number%100 == 0){
                        System.out.println(number);
                    }
                    number++;
                    //产生阻塞
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("任务执行完毕");
        };

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

3.2.4 while中的try/catch问题


  • 如果while里面放thr-catch,中断会失效

  • 原因:设计sleep函数时,在响应中断时,就会清除中断标记位 ,所以在停止线程时,不能将sleep的异常进行简单处理,应该抛出到更上级的方法进行处理。。 //todo 待完善

  • 使用代码演示

/**
 * Created by mucong on 2021/1/16 19:36
 *
     * 描述:  如果while里面放thr-catch,中断会失效
 */
public class CantInterrupt {

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

          while ( number<= 10000 ){
              if ( number%100 == 0 ){
                  System.out.println(number);
              }
              number++;
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        thread.sleep(5000);
        thread.interrupt();
    }
}

3.2.5 实际开发中的两种最佳实践之优先抛出


  • 由于sleep的特殊机制,我们需要设计一种避免清除中断标记位的,正确的停止线程的方法。。

  • 优先选择在方法上抛出异常。 用throws InterruptedException 标记你的方法,不采用try 语句块捕获异常,以便于该异常可以传递到顶层,让run方法可以捕获这一异常,例如:

void subTask() throws InterruptedException
  sleep(delay);
}
  • 优点:由于run方法内无法抛出checked Exception(只能用try catch),顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。

  • 使用代码演示

/**
 * Created by mucong on 2021/1/17 17:43
 *
 * 最佳实践1:    catch了InterruptException之后的优先选
 *              择:在方法签名中抛出异常,那么run()就会强制try/catch
 *
 */
public class RightWayStopThreadInProd implements Runnable{
    @Override
    public void run() {
        while ( true && !Thread.currentThread().isInterrupted()){

            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }
    //此方法使用throws抛出interrupt异常
    //如果使用try/catch则会吞掉这个阻塞
    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.2.6 实际开发中的两种最佳实践之恢复中断


  • 第二种解决方法:同样是避免sleep的特殊机制即清除中断标记位,

  • 思路:第一种方法是避免sleep的特殊机制,那当然可以是在出发了特殊机制后,再进行中断信号的标记即可====恢复中断

  • 使用:如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法,throws InterruptedException),那么应该选择在catch 子句中调用Thread.currentThread().interrupt() 恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。

  • 使用代码演示 : 在这里,线程在sleep期间被中断,并且由catch捕获到该中断,并重新设置了中断状态,以便于可以在下一个循环的时候检测到中断状态,正常退出。

 /**
 * Created by mucong on 2021/1/17 18:18
 *
 * 描述:  最佳实践2:在catch语句中调用Thread.currentThread().interrupt()来恢复设置
 *                  中断状态,以便在后续的执行中,依然能够检查到刚才发生的中断
 */
public class RightWayStopThreadInProd2 implements Runnable{
    @Override
    public void run() {
        while ( true ){

            if (Thread.currentThread().isInterrupted()){
                System.out.println("保存日志");
                break;
            }
            System.out.println("go");
            reInterrupt();
        }
    }
    //使用try/catch,并且在catch中恢复中断
    //Thread.currentThread().interrupt();
    private void reInterrupt()  {

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            //使用interrupt方法进行中断恢复
            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();
    }
}

3.2.7 响应中断的方法

---

- 以下方法都可以响应interrupt的中断信号

![](https://secure-static.wolai.com/static/rqMn9mPSv1r6f399FoxYSw/image.png)

![](https://secure-static.wolai.com/static/jxC6bvSM5jDdsxtaiKDNn7/image.png)

3.3 错误的停止方式


  • 被弃用的stopsuspendresume方法

  • volatile设置boolean标记位

volatile错误的解析:这种做法是错误的,或者说是不够全面的,在某些情况下虽然可用,但是某些情况下有严重问题。
这种方法在《Java并发编程实战》中被明确指出了缺陷,我们一起来看看缺陷在哪里:

  • 错误原因:如果我们遇到了线程长时间阻塞(这是一种很常见的情况,例如生产者消费者模式中就存在这样的情况),就没办法及时唤醒它,或者永远都无法唤醒该线程,而interrupt设计之初就是把wait等长期阻塞作为一种特殊情况考虑在内了,我们应该用interrupt思维来停止线程。

  • 代码演示part1:看似可行的代码

/**
 * Created by mucong on 2021/1/23 15:41
 * <p>
 * 描述:      演示volatile的局限:part1--看似可行
 */
public class WrongWayVolatile implements Runnable {
    //设置一个标记位
    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 10000 && !canceled) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍数");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatile r = new WrongWayVolatile();
        Thread thread = new Thread(r);

        thread.start();
        Thread.sleep(5000);
        r.canceled=true;

    }
}
  • 代码演示part2:不适用的情况
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * Created by mucong on 2021/1/23 16:48
 * <p>
 * 描述:    演示用volatile的局限part2:
 * 当线程阻塞是,volatile无法中断线程
 * eg:生产者生产速度过快,消费者消费速度慢,所以就会出现阻塞队列满了以后,
 * 生产者会阻塞,等待消费者消费。。
 */
public class WrongWayVolatileCantStop {

    public static void main(String[] args) throws InterruptedException {
        //当满的时候会放不进去,空的时候会取不出
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        //生产者线程
        Producer p1 = new Producer(storage);
        Runnable target;
        Thread producerThread = new Thread(p1);
        producerThread.start();
        Thread.sleep(1000);
        //消费者线程
        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()){
            System.out.println(consumer.storage.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了");

        //一旦消费者不再需要更多数据了,生产者应该停下,但是
        p1.cancel = true;
    }

}

class Producer implements Runnable {
    public volatile boolean cancel = false;
    BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 10000 && !cancel) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是100的倍数,被放到仓库了");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者停止运行");
        }
    }
}

class Consumer {
    BlockingQueue storage;

    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }
    //判断是不是要进行消费
    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }
}
  • 用中断的方式进行停止
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * Created by mucong on 2021/1/23 17:37
 * 描述:      用中断(interrupt)来修复刚才的错误
 */
public class WrongWayVolatileFixed {
    public static void main(String[] args) throws InterruptedException {

        WrongWayVolatileFixed body = new WrongWayVolatileFixed();
        //当满的时候会放不进去,空的时候会取不出
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        //生产者线程
        Producer p1 = body.new Producer(storage);
        Runnable target;
        Thread producerThread = new Thread(p1);
        producerThread.start();
        Thread.sleep(1000);
        //消费者线程
        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()){
            System.out.println(consumer.storage.take()+"被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了");

        //一旦消费者不再需要更多数据了,生产者应该停下,但是
        producerThread.interrupt();
    }
        //生产者
        class Producer implements Runnable {

            BlockingQueue storage;

            public Producer(BlockingQueue storage) {
                this.storage = storage;
            }

            @Override
            public void run() {
                int num = 0;
                try {
                    while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
                        if (num % 100 == 0) {
                            storage.put(num);
                            System.out.println(num + "是100的倍数,被放到仓库了");
                        }
                        num++;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("生产者停止运行");
                }
            }
        }
        //消费者
        class Consumer {
            BlockingQueue storage;

            public Consumer(BlockingQueue storage) {
                this.storage = storage;
            }
            //判断是不是要进行消费
            public boolean needMoreNums() {
                if (Math.random() > 0.95) {
                    return false;
                }
                return true;
            }

    }
}

3.4 停止线程的重要函数解析


  • public static boolean interrupted():判断是否要中断,并且清除当前的状态 ====目标对象:主线程

    • 源码如下:
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

  • public boolean isInterrupted() :判断是否要中断,并且不清除当前的状态 ====目标对象:调用的线程

    • 源码如下
public boolean isInterrupted() {
        return isInterrupted(false);
    }

3.5 常见面试问题


3.5.1如何停止线程

  • 按照以下顺序进行阐述:
  1. 原理:用interrupt来请求,好处

  2. 想停止线程需要请求方、被请求方、子方法被调用方相互配合

  3. 最后再说错误的方法:stop/suspend被弃用,volatile的boolean无法处理长时间阻塞问题

  • 具体如下:
  1. 原理:用interrupt来请求线程停止而不是强制,好处是安全

  2. 想停止线程,要请求方、被停止方、子方法被调用方相互配合才行:

- 作为被停止方:**每次循环**中或者适时**检查中断信号**,并且在可**能抛出InterrupedException的地方处理该中断信号**
- 请求方:发出中断信号;

- 子方法调用方(被线程调用的方法的作者)要注意:**优先在方法层面抛出InterrupedException,或者检查到中断信号时,再次设置中断状态**
  1. 最后再说错误的方法:stop/suspend已废弃,volatile的boolean无法处理长时间阻塞的情况

3.5.2.如何处理不可中断的阻塞

- 首先声明没有一个方法可以通吃所有的阻塞情况,然后进行如下描述

- 如果线程阻塞是由于调用了 wait(),sleep() 或 join() 方法,你可以中断线程,通过抛出 InterruptedException 异常来唤醒该线程。  但是对于不能响应InterruptedException的阻塞,很遗憾,并没有一个通用的解决方案。

- **但是**我们可以利用特定的其它的可以响应中断的方法,比如:

  - ReentrantLock.lockInterruptibly(),比如关闭套接字使线程立即返回等方法来达到目的。

  - 答案有很多种,因为有很多原因会造成线程阻塞,所以针对不同情况,唤起的方法也不同。

- **总结就是说如果不支持响应中断,就要用特定方法来唤起,没有万能药。**