停止基于线程的服务

应用程序通常会创建拥有多个线程的服务,例如线程池,并且这些服务的生命周期通常比创建它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束。由于无法通过抢占式的方法来停止线程,因此它们需要自行结束。

正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。例如,中断线程或者修改线程的优先级等。在线程API中,并没有对线程所有权给出正式的定义:线程由Thread 对象表示,并且像其他对象一样可以被自由共享。然而,线程有一个相应的所有者,即创建该线程的类。因此线程池是其工作者线程的所有者,如果要中断这些线程,那么应该使用线程池。

与其他封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。相反,服务应该提供生命周期方法(Lifecycle Method)来关闭它自己以及它所拥有的线程。这样,当应用程序关闭该服务时,服务就可以关闭所有的线程了。在ExecutorSc:vice 中提供了shutdown和shutdownNow等方法。同样,在其他拥有线程的服务中也应该提供类似的关闭机制。

对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。

   示例:日志服务

在大多数服务器应用程序中都会用到日志,例如,在代码中插入println语句就是一种简单的日志。像PrintWriter这样的字符流类是线程安全的,因此这种简单的方法不需要显式的同步。然而,在11.6节中,我们将看到这种内联日志功能会给一些高容量的(Highvolume)应

如果需在单条日志消息中写入多行,那么要通过客户端加锁来避免多个线程不正确地交错输果两个线程同时把多行栈追踪信息(Stack Trace)添加到同一个流中,并且每行信息对应一个println 调用,那么这些信息在输出中将交错在一起,看上去就是一些虽然庞大但却毫无意义的栈追踪信息。

用程序带来一定的性能开销。另外一种替代方法是通过调用log 方法将日志消息放入某个队列中,并由其他线程来处理。

在程序清单7-13 的LogWriter中给出了一个简单的日志服务示例,其中日志操作在单独的日志线程中执行。产生日志消息的线程并不会将消息直接写入输出流,而是由LogWriter 通过BlockingQueue 将消息提交给日志线程,并由日志线程写入。这是一种多生产者单消费者(Multiple-Producer, Single-Consumer)的设计方式:每个调用log的操作都相当于一个生产者,而后台的日志线程则相当于消费者。如果消费者的处理速度低于生产者的生成速度,那么BlockingQueue将阻塞生产者,直到日志线程有能力处理新的日志消息。

this. queue =new LinkedBlockingQueue<String>(CAPACITY);

this. logger =new LoggerThread(writer);

}

public void start(){logger. start();}

public void log(String msg) throws InterruptedException {

queue. put(msg);

}

private class LoggerThread extends Thread {

private final PrintWriter writer;

public void run(){

try {

while (true)

writer. println(queue. take());

}catch(InterruptedException ignored){

}finally {

writer. close();

}

}

}

}

为了使像LogWriter 这样的服务在软件产品中能发挥实际的作用,还需要实现一种终止日志线程的方法,从而避免使JVM无法正常关闭。要停止日志线程是很容易的,因为它会反复调用take,而take 能响应中断。如果将日志线程修改为当捕获到InterruptedException 时退出,那么只需中断日志线程就能停止服务。

然而,如果只是使日志线程退出,那么还不是一种完备的关闭机制。这种直接关闭的做法会丢失那些正在等待被写入到日志的信息,不仅如此,其他线程将在调用log时被阻塞,因为日志消息队列是满的,因此这些线程将无法解除阻塞状态。当取消一个生产者-消费者操作时,

需要同时取消生产者和消费者。在中断日志线程时会处理消费者,但在这个示例中,由于生产者并不是专门的线程,因此要取消它们将非常困难。

另一种关闭LogWriter的方法是:设置某个“已请求关闭”标志,以避免进一步提交日志消息,如程序清单7-14所示。在收到关闭请求后,消费者会把队列中的所有消息写入日志,并解除所有在调用log 时阻塞的生产者。然而,在这个方法中存在着竞态条件问题,使得该方法并不可靠。log 的实现是一种“先判断再运行”的代码序列:生产者发现该服务还没有关闭,因此在关闭服务后仍然会将日志消息放入队列,这同样会使得生产者可能在调用log时阻塞并且无法解除阻塞状态。可以通过一些技巧来降低这种情况的发生概率(例如,在宣布队列被清空之前,让消费者等待数秒钟),但这些都没有解决问题的本质,即使很小的概率也可能导致程序发生故障。


                     }                                             throw new IllegalStateException("logger is shut down");

为LogWriter提供可靠关闭操作的方法是解决竞态条件问题,因而要使日志消息的提交操作成为原子操作。然而,我们不希望在消息加入队列时去持有一个锁,因为put 方法本身就可以阻塞。我们采用的方法是:通过原子方式来检查关闭请求,并且有条件地递增一个计数器来“保持”提交消息的权利,如程序清单7-15中的LogService所示。

                        程序清单7-15向LogWriter添加可靠的取消操作                        

public class LogService {

private final BlockingQueue<String>queue;

private final LoggerThread loggerThread;

private final PrintWriter writer;

@GuardedBy("this") private boolean isShutdown;

@GuardedBy("this") private int reservations;

public void start(){loggerThread. start();}

public void stop(){

synchronized (this){isShutdown=true;}

loggerThread. interrupt();

}

public void log(String mag) throws InterruptedException {

synchronized (this){

if (isShutdown)

throw new IllegalStateException(…);

++reservations;

}

queue. put(msg);

}

private class LoggerThread extends Thread {

public void run(){

try {

while (true){

try {

synchronized (LogService. this){

if (isShutdown &&reservations ==0)

break;

}

String msg=queue. take();

synchronized (LogService. this){--reservations;}

writer. println(msg);

}catch (InterruptedException e){/*retry */}

}

}finally {

writer. close();

}

}

}

}

                                                                       

    关闭ExecutorService

在6.2.4节中,我们看到ExecutorService 提供了两种关闭方法:使用shutdown正常关闭,以及使用shutdownNow强行关闭。在进行强行关闭时,shutdownNow首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单。

这两种关闭方式的差别在于各自的安全性和响应性:强行关闭的速度更快,但风险也更大,因为任务很可能在执行到一半时被结束;而正常关闭虽然速度慢,但却更安全,因为ExecutorService会一直等到队列中的所有任务都执行完成后才关闭。在其他拥有线程的服务中也应该考虑提供类似的关闭方式以供选择。

简单的程序可以直接在main函数中启动和关闭全局的ExecutorService。而在复杂程序中,通常会将ExecutorService封装在某个更高级别的服务中,并且该服务能提供其自己的生命周期方法,例如程序清单7-16 中LogService的一种变化形式,它将管理线程的工作委托给一个ExecutorService,而不是由其自行管理。通过封装ExecutorService,可以将所有权链(Ownership Chain)从应用程序扩展到服务以及线程,所有权链上的各个成员都将管理它所拥有的服务或线程的生命周期。

                  程序清单7-16使用ExecutorService的日志服务                           

public class LogService {

private final ExecutorService exec =newSingleThreadExecutor();

...

public void start(){}

public void stop{) throws InterruptedException {

try {

exec. shutdown();

exec. awaitTermination(TIMEOUT,UNIT);

}finally {

writer. close();

}

}

public void log(String msg){

try {

exec. execute(new WriteTask(msg));

}catch (RejectedExecutionException ignored){}

}

}

                                                                    

    “毒丸”对象

另一种关闭生产者-消费者服务的方式就是使用“毒丸(Poison Pill)”对象:“毒丸”是指一个放在队列上的对象,其含义是:“当得到这个对象时,立即停止。”在FIFO(先进先出)队列中,“毒丸”对象将确保消费者在关闭之前首先完成队列中的所有工作,在提交“毒丸”对象之前提交的所有工作都会被处理,而生产者在提交了“毒丸”对象后,将不会再提交任何工作。在程序清单7-17、程序清单7-18 和程序清单7-19 中给出一个单生产者-单消费者的桌面搜索示例(来自程序清单5-8),在这个示例中使用了“毒丸”对象来关闭服务。

                        程序清单7-17通过“毒丸”对象来关闭服务                        

public class IndexingService {

private static final File POISON =new File("");

private final IndexerThread consumer =new IndexerThread();

private final CrawlerThread producer =new CrawlerThread();

private final BlockingQueue<File>queue;

private final FileFilter fileFilter;

private final File root;

class    CrawlerThread    extends Thread    {/*程序清单7-18*/}

class    IndexerThread extends Thread    {/*程序清单7-19 */.}

public void start(){

producer. start();

consumer. start();

}

public void stop(){producer. interrupt();}

public void awaitTermination() throws InterruptedException {

consumer. join();

}

}

                                                                    

               程序清单7-18      IndexingService的生产者线程                                 

public class CrawlerThread extends Thread {

public void run(){

try {

crawl(root);

}   catch    (InterruptedException e)   {/*   发生异常   */   }

finally {

while (true){

try {

queue. put(POISON);

break;

}catch    (InterruptedExceptione1)   {/*   重新尝试   */}

}

private void crawl(File root) throws InterruptedException {

                           程序清单7-19   IndexingService的消费者线程                              

public class IndexerThread extends Thread {

public void run(){

try {

while (true){

File file =queue. take();

if (file ==POISON)

break;

else

indexFile(file);

}

}catch (InterruptedException consumed){}

只有在生产者和消费者的数量都已知的情况下,才可以使用“毒丸”对象。在Indexing-Service 中采用的解决方案可以扩展到多个生产者:只需每个生产者都向队列中放入一个“毒丸”对象,并且消费者仅当在接收到Nproducers 个“毒丸”对象时才停止。这种方法也可以扩展到多个消费者的情况,只需生产者将Neonummers个“毒丸”对象放入队列。然而,当生产者和消费者的数量较大时,这种方法将变得难以使用。只有在无界队列中,“毒丸”对象才能可靠地工作。

  示例:只执行一次的服务

如果某个方法需要处理一批任务,并且当所有任务都处理完成后才返回,那么可以通过一个私有的Executor来简化服务的生命周期管理,其中该Executor的生命周期是由这个方法来控制的。(在这种情况下,invokeAll和invokeAny等方法通常会起较大的作用。)

程序清单7-20中的checkMail方法能在多台主机上并行地检查新邮件。它创建一个私有的Executor,并向每台主机提交一个任务。然后,当所有邮件检查任务都执行完成后,关闭Executor并等待结束。

                  

之所以采用AtomicBoolean来代替volatile 类型的boolean,是因为能从内部的Runnable 中访问hasNewMail 标志,因此它必须是final 类型以免被修改。

         程序清单7-20使用私有的 Executor,并且该 Executor 的生命周期受限于方法调用            

boolean checkMail(Set<String>hosts, long timeout,TimeUnit unit)

throws InterruptedException {

ExecutorService exec =Executors. newCachedThreadPool();

final AtomicBoolean hasNewMail =new AtomicBoolean(false);

try {

for (final String host :hosts)

exec. execute(new Runnable(){

public void run(){

if (checkMail(host))

hasNewMail. set(true);

}

} )  ;

}finally {

exec. shutdown();

exec. awaitTermination(timeout, unit);

}

return hasNewMail. get();

}

 shutdownNow的局限性

当通过shutdownNow 来强行关闭ExecutorService时,它会尝试取消正在执行的任务,并返回有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理。

然而,我们无法通过常规方法来找出哪些任务已经开始但尚未结束。这意味着我们无法在关闭过程中知道正在执行的任务的状态,除非任务本身会执行某种检查。要知道哪些任务还没有完成,你不仅需要知道哪些任务还没有开始,而且还需要知道当Executor关闭时哪些任务正在执行。

在程序清单7-21的TrackingExecutor 中给出了如何在关闭过程中判断正在执行的任务。通过封装ExecutorService 并使得execute(类似地还有submit,在这里没有给出)记录哪些任务是在关闭后取消的,TrackingExecutor 可以找出哪些任务已经开始但还没有正常完成。在Executor 结束后,getCancelledTasks返回被取消的任务清单。要使这项技术能发挥作用,任务在返回时必须维持线程的中断状态,在所有设计良好的任务中都会实现这个功能。

                  程序清单7-21      在ExecutorService中跟踪在关闭之后被取消的任务                  

public class TrackingExecutor extends AbstractExecutorService {

private final ExecutorService exec;

private final Set<Runnable>tasksCancelledAtShutdown =

                 

shutdownNow 返回的Runnable 对象可能与提交给ExecutorService的Runnable 对象并不相同:它们可能是被封装过的已提交任务。

⇒_然而,在关闭程中只会返回尚未开始的任务,而不会返回正在执行的任务。如果能返回所有这两种类型的任务,那么就不需要这种不确定的中间状态。

Collections. synchronizedSet(new HashSet<Runnable>());

...

public List<Runnable>getCancelledTasks(){

if (lexec. isTerminated())

throw new IllegalStateException(...);

return new ArrayList<Runnable>(tasksCancelledAtShutdown);

}

public void execute(final Runnable runnable){

exec. execute(new Runnable(){

public void run(){

try {

runnable. run();

}finally {

if (isShutdown()

&&Thread. currentThread(). isInterrupted())

tasksCancelledAtShutdown. add(runnable);

} )  ;

}

//将ExecutorService的其他方法委托给exec

}

在程序清单7-22 的WebCrawler 中给出了TrackingExecutor的用法。网页爬虫程序的工作通常是无穷尽的,因此当爬虫程序必须关闭时,我们通常希望保存它的状态,以便稍后重新启动。CrawlTask 提供了一个getPage 方法,该方法能找出正在处理的页面。当爬虫程序关闭时,无论是还没有开始的任务,还是那些被取消的任务,都将记录它们的URL,因此当爬虫程序重新启动时,就可以将这些URL 的页面抓取任务加入到任务队列中。

public abstract class WebCrawler {

private volatile TrackingExecutor exec;

@GuardedBy("this")

private final Set<URL>urlsToCrawl =new HashSet<URL>();

...

public synchronized void start(){

exec =new TrackingExecutor(

Executors,newCachedThreadPool());

for (URL url:urlsToCrawl)submitCrawlTask(url);

urlsToCrawl. clear();

}

public synchronized void stop() throws InterruptedException {

try {

saveUncrawled(exec. shutdownNow());

if (exec. awaitTermination(TIMEOUT,UNIT))

saveUncrawled(exec. getCancelledTasks());

}finally {

exec =null;

}

}

protected abstract List<URL>processPage(URL url);

private void saveUncrawled(List<Runnable>uncrawled){

for (Runnable task :uncrawled)

urlsToCrawl. add(((CrawlTask) task). get Page());

}

private void submitCrawlTask(URL u){

exec. execute(new CrawlTask(u));

}

private class CrawlTask implements Runnable {

private final URL url;

...

public void run(){

for (URL link :processPage(url)){

if (Thread. current Thread(). isInterrupted())

return;

submitCrawlTask(link);

}

}

public URL getPage(){return 'url;}

}

}

在TrackingExecutor中存在一个不可避免的竞态条件,从而产生“误报”问题:一些被认为已取消的任务实际上已经执行完成。这个问题的原因在于,在任务执行最后一条指令以及线程池将任务记录为“结束”的两个时刻之间,线程池可能被关闭。如果任务是幂等的(Idempotent,即将任务执行两次与执行一次会得到相同的结果),那么这不会存在问题,在网页爬虫程序中就是这种情况。否则,在应用程序中必须考虑这种风险,并对“误报”问题做好准备。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心是凉的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值