5.3 Two-phase Termination模式实战案例解析
某系统的告警功能被封装在一个模块中。告警模块的入口类是AlarmMgr。其他模块(业务模块)需要发送告警信息时只需要调用AlarmMgr的sendAlarm方法即可。该方法将告警信息缓存入队列,由专门的告警发送线程负责调用AlarmAgent的相关方法发送告警信息。AlarmAgent类负责与告警服务器对接,它通过网络连接将告警信息发送给告警服务器。
告警发送线程是一个用户线程(User Thread),因此在系统停止过程中,该线程若未停止则会阻止Java虚拟机正常关闭。所以,在系统停止过程中,我们必须主动停止告警发送线程,而非依赖Java虚拟机。为了能够尽可能快地以优雅的方式将告警发送线程停止,我们需要处理以下两个问题。
1. 当告警缓存队列非空时,需要将队列中已有的告警信息发送给告警服务器。
2. 由于缓存告警信息的队列是一个阻塞队列(ArrayBlockingQueue),因此在该队列为空的情况下,告警发送线程会一直处于等待状态。这会导致其无法响应我们关闭线程的请求。
上述问题可以通过使用Two-phase Termination模式来解决。
AlarmMgr相当于图5-1中的ThreadOwner参与者实例,它是告警发送线程(对应实例变量alarmSendingThread)的拥有者。在系统停止过程中调用其shutdown方法(AlarmMgr.getInstance.shutdown),即可请求告警发送线程停止。其代码如清单5-1所示。
清单5-1 AlarmMgr类源代码
告警发送线程类AlarmSendingThread的源代码,如清单5-2所示。
清单5-2 AlarmSendingThread类源代码
从上面的代码中可以看出,AlarmSendingThread每接收一个告警信息并放入缓存队列,便将terminationToken的reservations属性值增加1,而每发送一个告警信息给告警服务器,则将terminationToken的reservations属性值减少1。这为我们可以在停止告警发送线程前确保队列中现有的告警信息会被处理完毕提供了线索:AbstractTerminatableThread的run方法会根据terminationToken的reservations属性值是否为0来判断待停止的线程是否已无未处理完毕的任务,或者无须关心其是否有待处理的任务。
AbstractTerminatableThread类源代码如清单5-3所示。
清单5-3 AbstractTerminatableThread类源代码
AbstractTerminatableThread是一个可复用的Terminatable参与者实例。其terminate方法完成了线程停止的准备阶段。该方法首先将terminationToken的toShutdown属性设置为true,指示目标线程可以准备停止了。但是,此时目标线程可能处于一些阻塞(Blocking)方法的调用中,如调用Object.sleep、InputStream.read等,而无法检测toShutdown属性的值。调用目标线程的interrupt方法可以使一些阻塞方法(参见表5-1)抛出异常,从而使目标线程停止。但也有一些阻塞方法(如InputStream.read)并不对interrupt方法调用做出响应,此时需要由AbstractTerminatableThread的子类实现doTerminate方法,在该方法中实现一些关闭目标线程所需的额外操作。例如,在Socket同步I/O中通过关闭Socket使得使用该Socket的线程若处于I/O等待状态则会抛出SocketException。因此,terminate方法下一步调用doTerminate方法。接着,若terminationToken.reservations的值为非正数(表示目标线程无待处理的任务,或者我们不关心其是否有待处理的任务),则terminate方法会调用目标线程的interrupt方法,强制目标线程的阻塞方法中断,从而强制停止目标线程。
执行阶段在AbstractTerminatableThread的run方法中完成。该方法通过判断TerminationToken的toShutdown属性和reservations属性或者通过捕获由interrupt方法调用而抛出的异常来停止线程,并在线程停止前调用由AbstractTerminatableThread子类实现的doCleanup方法来执行一些清理动作。
在执行阶段,由于AbstractTerminatableThread.run方法在每次执行线程处理逻辑(通过调用doRun方法实现)前都先判断toShutdown属性和reservations属性的值,在目标线程处理完待处理的任务后(此时reservations属性的值为非正数),目标线程run方法也会退出for循环。因此,线程的处理逻辑方法将不再被调用,从而使本案例在不使用Two-phase Termination模式的情况下停止目标线程存在的两个问题得以解决(目标线程停止前可以保证处理完待处理的任务——发送队列中现有的告警信息给告警服务器)和规避(在目标线程发送完队列中现有的告警信息后,doRun方法不再被调用,从而避免了队列为空时BlockingQueue.take调用导致的阻塞)。
由上可知,准备阶段、执行阶段需要通过TerminationToken作为“中介”来协调二者的动作。TerminationToken类源代码如清单5-4所示。
清单5-4 TerminationToken类源代码