Wetts's blog

Stay Hungry, Stay Foolish.

0%

Java多线程编程实战指南-第5章-Two-phase Termination(两阶段终止)模式

读书笔记 - Java 多线程编程实战指南


第五章 Two-phase Termination(两阶段终止)模式

模式介绍

  • 停止线程时一个目标简单而实现却不那么简单的任务。首先,Java 没有提供直接的 API 用于停止线程(ava.lang.Thread 类的 stop 方法早已被不提倡使用了)。此外,停止线程还有一些额外的细节需要考虑,如待停止的线程处于阻塞(如等待锁)或者等待状态(等待其他线程)、尚有未处理完的任务等。
  • Two-phase Termination 模式通过将停止线程这个动作分解为准备阶段和执行阶段这两个阶段,提供了一种通用的用于优雅地停止线程的方法。

准备阶段: 该阶段的主要动作是“通知”目标线程(欲停止的线程)准备进行停止。这一步会设置一个标志变量用于指示目标线程可以准备停止了。但是,由于目标线程可能正在处于阻塞状态(等待锁的获得)、等待状态(如调用 Object.wait)或者 I/O(如 InputStream.read)等,即便设置了这个标志,目标线程也无法立即“看到”这个标志而做出相应动作。因此,这一阶段还需要通过调用目标线程的 interrupt 方法,以期待目标线程能够通过捕获相关的异常侦测到该方法调用,从而中断其阻塞状态、等待状态。目标线程可以通过捕获这些方法抛出的 InterruptedException 来侦测线程停止信号。但也有一些方法(如 InputStream.read)并不对 interrupt 调用作出响应,此时需要我们手工处理,如同步的 Socket I/O 操作中通过关闭 socket 是处于 I/O 等待的 socket 抛出 java.net.SocketException。
执行阶段: 该阶段的主要动作是检查准备阶段所设置的线程停止标志和信号,在此基础上决定线程停止的时机,并进行适当的“清理”操作。

-

架构模式

-

模式案例

某系统的告警功能被封装在一个模块中。告警模块的入口类是 AlarmMgr。其他模块(业务模块)需要发送告警信息时只需要调用 AlarmMgr 的 sendAlarm 方法即可。该方法将告警信息缓存入队列,由专门的告警发送线程负责调用 AlarmAgent 的相关方法发送告警。AlarmAgent 类负责与告警服务对接,它通过网络连接将告警信息发送至告警服务器。
告警发送线程是一个用户线程(User Thread),因此在系统的停止过程中,该线程若未停止则会阻止 JVM 正常关闭。所以,在系统停止过程中我们必须主动去停止告警发送线程,而非依赖 JVM。为了能够尽可能快地以优雅的方式将告警发送线程停止,我们需要处理一下两个问题:

  1. 当告警缓存队列非空时,需要将队列中已有的告警信息发送至告警服务器。
  2. 由于缓存告警信息的队列是一个阻塞队列(ArrayBlockingQueue),在该队列为空的情况下,告警发送线程会一直处于等待状态。这会导致其无法响应我们关闭的请求。

本案例 Demo 详见 multithreading 工程的 com.Two-phaseTermination 包

-

Two-phase Termination 模式的评价与实现考量

  • Two-phase Termination 模式使得我们可以对各种形式的目标线程进行优雅的停止。如目标线程调用了能够对 interrupt 方法调用作出响应的阻塞方法、目标线程调用了不能对 interrupt 方法调用做出响应的阻塞方法、目标线程作为消费者处理其他线程生存的“产品”在其停止前需要处理完现有“产品”等。Two-phase Termination 模式实现的线程停止可能出现延迟,即客户端代码调用完 ThreadOwner.shutdown 后,该线程可能仍在运行。
  • 需要注意一下几个问题:

线程停止标志
生产者-消费者问题中的线程停止
隐藏而非暴露可停止的线程

-

Java 标准库实例

类 java.util.concurrent.ThreadPoolExecutor 就使用了 Two-phase Termination 模式来停止其内部维护的工作者线程。