Java多线程编程实战指南:设计模式篇(第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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

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

本章案例展示了一个可复用的Two-phase Termination模式实现代码。读者若要加深对该模式的理解或者自行实现该模式,需要注意以下几个问题。

5.4.1 线程停止标志

本章案例使用了TerminationToken作为目标线程可以准备停止的标志。从清单5-4的代码中可以看到,TerminationToken使用了toShutdown这个boolean变量作为主要的停止标志,而未使用Thread.isInterrupted。这是因为,调用目标线程的interrupt方法无法保证目标线程的isInterrupted方法的返回值为true:目标线程可能调用一些代码,它们在捕获InterruptedException后没有通过调用Thread.currentThread.interrupt保留线程中断状态。另外,toShutdown这个变量为了在保证内存可见性的同时避免使用显式锁的开销,采用了volatile修饰。这点也很重要,笔者曾经见过一些采用boolean变量作为线程停止标志的代码,只是这些变量没有用volatile修饰,对其访问也没有加锁,这就导致可能无法停止目标线程。

另外,在某些场景下,多个可停止线程实例可能需要共用一个线程停止标志。例如,多个可停止线程实例“消耗”同一个队列中的数据。当该队列为空且不再有新的数据加入队列的时候,所有“消耗”该队列数据的可停止线程都应该被停掉。AbstractTerminatableThread类(源代码见清单5-3)的构造器支持传入TerminationToken实例,就是为了支持这种场景。

5.4.2 生产者/消费者问题中的线程停止

在多线程编程中,许多问题和一些多线程编程模式都可以看作生产者/消费者问题。停止处于生产者/消费者问题中的线程,需要考虑更多的问题。例如需要注意线程的停止顺序。如果消费者线程比生产者线程先停止,则会导致生产者生产的新“产品”无法被处理,而如果先停止生产者线程,又可能使消费者线程处于空等待(如生产者、消费者采用阻塞队列中转“产品”)。另外,停止消费者线程前是否考虑要等待其处理完所有待处理的任务或者将这些任务做备份也是一个问题。本章案例部分地展示了生产者/消费者问题中线程停止的处理过程,其核心就是使用TerminationToken的reservations属性:生产者每“生产”一个产品,Two-phase Termination模式的客户端代码都要使reservations属性值增加1(即调用terminationToken.reservations.incrementAndGet);消费者线程每“处理”一个产品,该线程的线程处理逻辑方法doRun都要使reservations属性值减少1(即调用terminationToken.reservations.decrementAndGet)。当然,在停止消费者线程时如果我们不关心其待处理的任务,Two-phase Termination模式的客户端代码可以忽略对reservations属性的操作。清单5-5展示了一个完整的生产者/消费者问题中线程停止的例子。

清单5-5 生产者/消费者问题中线程停止的例子

5.4.3 隐藏而非暴露可停止线程

为了保证可停止线程不被其他代码误停止,一般我们将可停止线程隐藏在线程拥有者背后,而使系统中的其他代码无法直接访问该线程,正如案例代码(见清单5-1)所展示的:AlarmMgr定义了一个private字段alarmSendingThread,用于引用告警发送线程(可停止线程),系统中的其他代码只能通过调用AlarmMgr的shutdown方法来请求该线程停止,而非通过引用该线程对象自身来停止它。