这章的主要内容是关于如何使任务和线程安全,快速,可靠的停止下来。
7.1 任务取消
在Java中没有一种安全的抢占方式来停止线程,但是可以使用一些协作机制,比如:
让素数生成器运行1秒后取消(并不会刚好在运行1秒后停止,因为在请求取消的时刻和run方法中循环执行下一次检查之间可能存在延迟):
-7.1.1 中断
上面的取消方法有个重要的问题是:如果任务中调用了一个阻塞方法,例如BlockingQueue.put,那么任务可能永远不会检查取消标志,因此永远不会结束。比如:
前面第五章曾提到,一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
阻塞方法库,如Thread.sleep和Object.wait等,都会检查线程何时中断,并且在发现中断时提前返回。
下面来解决之前BrokenPrimeProducer中永远检查不到标志位的问题:使用中断而不是boolean标志来请求取消
-7.1.2 中断策略
*对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码),应该小心地保存中断状态,这样拥有线程的代码才能对中断作出响应。
*大多数可阻塞的库函数知识抛出interruptedException作为中断响应,尽快退出流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。
*当检查到中断请求时,任务并不需要放弃所有的操作——它可以推迟处理中断请求,并直到某个更合适的时刻。因此需要记住中断请求,并在完成当前任务后抛出InterruptedException或者表示已收到中断请求。
-7.1.3 响应中断
对于一些不支持取消但仍可以调用中断的阻塞方法的操作,它们必须在循环中调用这些方法(interrput方法), 并在发现中断后重新尝试。在这种情况下应将中断状态保存在本地,并在返回前恢复状态而不是在捕获InterruptedException时恢复状态:
这部分有点看不明白。。。 先pass,以后再来钻研钻研
-7.1.4 示例:计时运行
给出了在指定时间内运行一个任意的Runnable的示例。在调用线程中运行任任务,并安排了一个取消任务,在运行指定的时间间隔后中断它。这解决了从任务中抛出为检查异常的问题,因为该异常会被timedRun的调用者捕获。下面的程序用到了ScheduledExecutorService。ScheduledExecutorService定时周期执行指定的任务
这是一种简单的方法,但却破坏了一下规则:在中断线程之前,应该了解它的中断策略。由于timedRun可以从任意一个线程调用,因此无法知道这个调用线程的中断策略(说到这里我好像把中断策略所针对的对象搞混淆了)。
在join方法返回后,它将检查任务中是否有异常抛出( task.rethrow() ) 如果有的话则会在timeRunde的线程中再次抛出异常。执行任务的线程拥有自己的执行策略,即使任务不响应中断,限时运行的方法(join)仍能返回到它的调用者。
join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回。
PS:这一小节看得有点云里雾里的,汉化质量实在是太垃圾了,哪天翻翻英文原版书,看看这块的解释。
-7.1.5 通过Future来实现取消
最后那句话是神马意思? 醉了,语文没学好的表示很蛋疼。
-7.1.6 处理不可中断的阻塞
在java的库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞的方法或者阻塞机制都能响应中断,中断请求只能设置线程的中断状态,除此之外没有任何其他作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。
-7.1.7 采用newTaskFor来封装非标准的取消
newTaskFor是一个工厂方法,它将创建Future来代表任务,还能返回一个RunnableFuture接口,该接口扩展了Future和Runnable(并有FutureTask实现)。通过定制表示任务的Future可以改变Future.cancel的行为。那个程序来演示:
这部分看得我有点云里雾里的, 以后刷二周目的时候再来仔细分析分析。
7.2 停止基于线程的服务
*应用程序通常会创建拥有多个线程的服务
*应用程序可以拥有服务,服务可以拥有工作线程,但应用程序并不能拥有工作线程,因此应用程序不能直接停止工作线程。
-7.2.1 示例:日志服务
现在还需要实现一种终止日志线程的方法,从而避免使JVM无法正常关闭。take方法能响应中断,如果将日志线程修改为当捕获到InterruptedException时退出,那么只需中断日志线程就能停止服务。然而,如果只是使日志线程退出,那么还不是一种完备的关闭机制。这种直接关闭的做法会丢失哪些正在等待被写入到日志的信息,而且其他线程在调用log时将被阻塞。另一种关闭LogWriter的方法是:设置某个“已请求关闭”标志,以避免进一步提交日志信息:
在收到关闭请求后,消费者会把队列中的所有消息写入日志,并解除所有在调用log时阻塞的生产者(我怎么感觉这里书这里和代码对不上号。。。)。然而这个方法中存在着竞态条件问题,使得该方法并不可靠。向LogWriter添加可靠的取消操作:
-7.2.2 关闭ExecutorService
在复杂的程序中,通常会将ExecutorService封装在某个更高级别的服务中,并且该服务能提供其自己的生命周期方法。如:
-7.2.3 “毒丸”对象
另一种关闭生产者-消费者服务的方式是使用“毒丸”对象,当得到这个对象时立即停止。“毒丸”对象确保消费者在关闭之前首先完成了队列中的所有工作。来个例子:
只有在生产者和消费者的数量都已知的条件下,才可以使用“毒丸”对象。上述的解决方案可以扩展到多个生产者:只需每个生产者都向队列中放入一个毒丸对象,并且消费者仅当在接收到Nproducers个毒丸对象时才停止。
-7.2.4 示例:只执行一次分服务
下面程序的checkMail方法能在多台主机上并行地检查新邮件。它创建一个私有的Executor,并向每台主机提交一个任务。然后,当所有邮件检查任务都执行完成后,关闭Executor并等待结束。
ExecuteService提供的shutdown方法:平滑的关闭ExecutorService,当此方法被调用时,ExecutorService停止接收新的任务并且等待已经提交的任务(包含提交正在执行和提交未执行)执行完成。当所有提交任务执行完毕,线程池即被关闭。
awaitTermination方法:接收timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
-7.2.5 shutdownNow的局限性
当通过shutdownNow来强行关闭ExecuteService时,它会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理。但是,我们无法通过常规方法来找出哪些任务已经开始但尚未结束。意思就是虽然shutdownNow会返回所有已提交但尚未开始的任务(都是Runnable), 但是却返回不了已经在执行但还未结束的任务,只能将这些任务取消掉。有的时候我们需要知道并保存这个状态,所以不仅要知道哪些任务还没有开始,而且还要知道哪些正在执行的任务还没有完成。
下面的程序给出了如何在关闭过程中判断正在执行的任务。
在程序清单7-22的WebCrawler中给出了TrackingExecutor的用法。网页爬虫程序的工作通常是无穷无尽的,因此当爬虫程序关闭时,我们通常希望保存它的状态(这里的意思就是要保存程序正在处理的网页),以便稍后重新启功时继续处理这些网页。
在TrackingExecutor中存在一个不可避免的竞态条件,从而产生误报问题:一些认为已取消的任务实际上已经执行完成。原因是在任务执行最后一条指令以及线程池将任务记录为“结束”的两个时刻之间,线程池可能被关闭。如果任务是幂等的(即将任务执行两次的结果与执行一次会得到相同的结果),那么这不会存在问题,否则需考虑这种风险。
7.3 处理非正常的线程中止
首先应该明确的是导致线程提前死亡的最主要原因是RuntimeException,如何处理这种非正常的线程中止来确保多线程情况下的线程安全性呢?简而言之就是利用捕获异常处理器和故障通知机制。下面的程序给出了如何在线程池内部构建一个工作者线程:
上面的是一种主动的方法来解决为检查异常。在Thread API中提供了UncaughtExceptionHandler,它能检测出由于未捕获的异常而终结的情况。当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给UncaughtExceptionHandler异常处理器。
最常见的响应方式是将一个错误信息以及相应的栈追踪信息写入应用程序中:
要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。
7.4 JVM关闭
-7.4.1 关闭钩子
正常的JVM关闭中,JVM首先调用所有已注册的关闭钩子。这个关闭钩子指的是通过Runtime.addShutdownHook注册但尚未开始的线程。后面看不明白了,pass....
-7.4.2 守护线程
线程可以分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(如垃圾回收器以及其他执行辅助工作的线程)。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出。当JVM停止时,所有仍然存在的守护线程都将被抛弃,既不会执行finally代码块,也不会执行回卷栈。
应该尽可能少使用守护线程——很少有操作能够在不进行清理的情况下被安全地抛弃。特别是,如果在守护线程中执行可能包含I/O操作的任务将会是一种危险的行为。
-7.4.3 终结器
有些资源如文件句柄或套接字,当不需要时必须现实地交还给操作系统,而不是通过垃圾回收器回收。为了实现这个功能,垃圾回收器对哪些定义了finalize方法的对象会经行特殊处理:在回收器释放它们后。调用它们的finalize方法,从而保证一些持久化的资源被释放。大多数情况下,通过使用finally代码块和显示的close方法,能够比终结器更好的管理资源,所以要避免使用终结器。