Java 并发系列十五 : 两阶段终止模式-如何优雅地终止线程?

前言

感谢王宝令老师的并发编程课程

今天咱们从技术的角度聊聊如何优雅的终止一个线程。线程执行完毕或者出现异常就会进入终止状态,这样看终止 一个线程很简单啊,不过我们今天谈的是“优雅地终止线程”, 不是自己终止自己,而是一个在T1 中,终止线程 T2; 这里所谓的“优雅”, 指的是给 T2 一个机会料理后事,而不是被一剑封喉。

Java 语言的 Thread 类中曾经提供了一个 stop() 方法, 用来终止线程。但是早已不建议使用,原因就是这个方法的作用就是一剑封喉的做法,被终止的线程没有料理后事的机会。

既然不建议使用,那如何优雅的终止线程呢?

如何理解两阶段终止模式

前辈们已经总结出一套成熟的方案,叫做两阶段终止模式。顾名思义就是就是将终止过程分为两阶段,第一个阶段就是线程T1向线程T2 发送终止指令,而第二阶段就是线程T2 响应终止指令。

img

那在 Java 语言里,终止指令是什么呢?这个要从 Java 线程的状态转换过程说起。

img

从这个图你会发现,Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态, 而实际上线程可能处于休眠状态,也就是说,我们想要终止一个线程,首先要把线程的状态从休眠状态转换到RUNNABLE 状态。 这个如何做到?

这个要靠 Java Thread 类提供的 interrupt() 方法,它可以将休眠状态的线程转换到 RUNNABLE 状态。

线程转换到RUNNABLE 状态之后,我们然后将其终止 呢?RUNNABLE 切换到种子状态,优雅的方式就是让Java 线程自己执行完run() 方法,所以我们一般采用的方法是设置一个表示位,然后线程会在合适的时机检查这个标识位,如果发现这个终止条件,则自动退出run() 方法 。这个就是我们前面提到的 第二终止阶段:响应终止指令。

下面让我们来看一个实际中作种的案例。

用两阶段终止模式终止监控操作

实际工作中,有些监控系统需要动态的采集一些数据,一般都是监控系统发给需要监控的系统的监控代理,监控代理接到监控指令后,从监控目标收集数据,然后回传给监控系统,如下图。处于对性能的考虑(有些监控项对形态性能影响大,所以不能一直持续监控)动态采集功能一般都会有终止操作。

img

强烈建议你设置自己的线程终止标志位,例如在下面的代码中,使用 isTerminated 作为线程终止标志位,此时无论是否正确处理了线程的中断异常,都不会影响线程优雅地终止。


class Proxy {
  //线程终止标志位
  volatile boolean terminated = false;
  boolean started = false;
  //采集线程
  Thread rptThread;
  //启动采集功能
  synchronized void start(){
    //不允许同时启动多个采集线程
    if (started) {
      return;
    }
    started = true;
    terminated = false;
    rptThread = new Thread(()->{
      while (!terminated){
        //省略采集、回传实现
        report();
        //每隔两秒钟采集、回传一次数据
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e){
          //重新设置线程中断状态
          Thread.currentThread().interrupt();
        }
      }
      //执行到此处说明线程马上终止
      started = false;
    });
    rptThread.start();
  }
  //终止采集功能
  synchronized void stop(){
    //设置中断标志位
    terminated = true;
    //中断线程rptThread
    rptThread.interrupt();
  }
}

如何优雅地终止线程池

Java 领域用的最多的还是线程池,而不是手动地创建线程。那我们该如何优雅地终止线程池呢? 线程池提供了两个方法:shutdown()和shutdownNow()。 了解它们的区别,就先需要了解线程池的实现原理。

我们曾经讲过 Java 线程池是生产者 - 消费者模式的一种实现, 提交给线程池的任务,首先是进入一个阻塞队列中,之后线程池中的线程从队列中取出任务执行。

shutdown() 方法是一种很保守的关闭线程池的方法。 线程池执行完shutdown() 后,就会拒绝接收新的任务,但是会等待正在执行的任务或者队列中的任务执行完毕再关闭线程池。

而 shutdownNow() 方法,相对就激进一些了,线程池执行 shutdownNow() 后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为 shutdownNow() 方法的返回值返回。因为 shutdownNow() 方法会中断正在执行的线程,所以提交到线程池的任务,如果需要优雅地结束,就需要正确地处理线程中断。

如果提交到线程池的任务不允许取消,那就不能使用 shutdownNow() 方法终止线程池。不过,如果提交到线程池的任务允许后续以补偿的方式重新执行,也是可以使用 shutdownNow() 方法终止线程池的。《Java 并发编程实战》这本书第 7 章《取消与关闭》的“shutdownNow 的局限性”一节中,提到一种将已提交但尚未开始执行的任务以及已经取消的正在执行的任务保存起来,以便后续重新执行的方案,你可以参考一下,方案很简答,这里就不详细介绍了。

其实分析完 shutdown() 和 shutdownNow() 方法你会发现,它们实质上使用的也是两阶段终止模式,只是终止指令的范围不同而已,前者只影响阻塞队列接收任务,后者范围扩大到线程池中所有的任务。

总结

两阶段提交模式是一种应用很广泛的并发设计模式,在 Java 语言中,使用两阶段终止模式来优雅的终止线程。需要两个关键点:一个是仅检查终止标识位是不够的,因为线程的状态可能处于休眠状态。另一个是仅检查线程的中断状态也是不够的,因为我们依赖的第三方类库很可能没有正确处理中断异常。

当使用Java 的线程池来管理线程的时候,需要依赖线程池提供的shutdown() 和 shutdownNow() 方法来终止线程。不过再使用的时候要注意他们的应用场景。尤其是使用shutdownNow() 的时候,一定要谨慎。

你可能感兴趣的:(Java 并发系列十五 : 两阶段终止模式-如何优雅地终止线程?)