@@@ 要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java 没有提供
任何机制来安全地终止线程。但它提供了中断(interruption),这是一种协作机制,能够使一个
线程终止另一个线程的当前工作。
@@@ 在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清除当前正在
执行的工作,然后再结束,这提供了更好的灵活性,因为任务本身的代码比发出取消请求的代码
更清楚如何执行清除工作。
@@@ 生命周期结束(End-of-Lifecycle)的问题会使任务、服务以及程序的设计和实现等过程
变得复杂,而这个在程序设计中非常重要的要素却经常被忽略。
@@@ 一个行为良好的软件和勉强运行的软件之间的最主要的区别就是:行为良好的软件能很
完善地处理失败 、 关闭和取消等过程。
》》任务取消
@@@ 如果外部代码能在某个操作正常完成之前将其置入 “ 完成 ” 状态 ,那么这个操作就可以
称为可取消的(Cancellable)。取消某个操作的原因有很多:
--------- 用户请求取消
用户点击图形界面程序中的 “ 取消 ” 按钮,或者通过管理接口来发出取消请求,如 JMX
--------- 有时间限制的操作
--------- 应用程序事件
例如,应用程序对某个问题空间进行分解并搜索,从而使不同的任务可以搜索问题空间
中的不同区域。当其中一个任务找到了解决方案时,所有其他仍在搜索的任务都将被取消。
--------- 错误
---------- 关闭
@@@ 在 Java 中没有一种安全的抢占方法来停止线程,因此也就没有安全的抢占方法来停止
任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
其中一种协作机制能设置某个 “ 已请求取消 ” 标志,而任务将定期地查看该标志。如果
设置了该标志,那么任务将提前结束。
@@@ 一个可取消的任务必须拥有取消策略(Cancellation Policy), 在这个策略中将详细地
定义取消操作的 “ How ” 、 “ When ” 、以及 “ What ” ,即其他代码如何(How)请求取消该任
务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行哪些(What)
操作。
### 中断
@@@ 一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以通过这种机制来
通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
@@@ 在 Java API 或语言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在
取消之外的其他操作中使用中断,那么都是不合适的,并且很难支撑起更大的应用。
@@@ 每个线程都有一个 boolean 类型的中断状态。当中断线程时,这个线程的中断状态
将被设置为 true 。在 Thread 中包含了中断线程以及查询线程中断状态的方法。
---------------------------------------------------------------------------------------------------------------------------------------
public class Thread {
public void interrupt( ){ // 能中断目标线程
}
public boolean isInterrupted ( ){ // 能返回目标线程的中断状态
}
public static boolean interrupted( ){ // 将清除当前线程的中断状态,这也是
} 清除中断状态的唯一方法
}
-----------------------------------------------------------------------------------------------------------------------------------------
@@@ 阻塞库方法,例如 Thread.sleep 和 Object.wait 等,都会检查线程何时中断,并且在
发现中断时提前返回。它们在响应中断时执行的操作包括: 清除中断状态,抛出
InterruptedException , 表示阻塞操作由于中断而提前结束。
JVM 并不能保证阻塞方法检测到中断的速度,但在实际情况中响应速度还是非常快的。
@@@ 调用 interrupt 并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断
的消息。
@@@ 对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断
请求,然后由线程在下一个合适的时刻中断自己。
@@@ 在使用静态的 interrupted 时应该小心,因为它会清除当前线程的中断状态。如果在
调用 interrupted 时返回了 true ,那么除非你想屏蔽这个中断,否则必须对它进行处理------
可以抛出 InterruptException 或者通过再次调用 interrupt 来恢复中断状态。
@@@ 通常,中断是实现取消的最合理方式。
@@@ 如果可中断的阻塞方法的调用频率并不高,不足以获得足够的响应性,那么显式地
检测中断状态能起到一定的帮助作用。
### 中断策略
@@@ 中断策略规定线程如何解释某个中断请求--------当发现中断请求时,应该做哪些工作
(如果需要的话),哪些工作单元对于中断来说是原子操作,以及以多块的速度来响应中断。
@@@ 最合理的中断策略是某种形式的线程级(Thread--Level)取消操纵或服务级(Service--
Level)取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。
@@@ 区分任务和线程对中断的反应是很重要的。一个中断请求可以有一个或多个接收者----
中断线程池中的某个工作者线程,同时意味着 “ 取消当前任务 ” 和 “ 关闭工作者线程 ” 。
@@@ 任务不会在其自己拥有的线程中执行,而是在某个服务(例如线程池)拥有的线程中
执行。
@@@ 大多数可阻塞的库函数都只是抛出 InterruptedException 作为中断响应。它们永远
不会在某个由自己拥有的线程中执行,因此它们为任务或库代码实现了最合理的取消策略:尽快
退出流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。
@@@ 无论任务把中断视为取消,还是其他某个中断响应操作,都应该小心地保存执行线程的
中断状态。
@@@ 线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的
取消机制中,例如关闭(shutdown)方法。
@@@ 由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应
该中断这个线程。
@@@ Java 的中断功能:强迫开发人员必须处理 InterruptedException 。然而,通过推迟中断
请求的处理,开发人员能制定更灵活的中断策略,从而使应用程序在响应性和健壮性之间实现合理
的平衡。
### 响应中断
@@@ 当调用可中断的阻塞函数时,例如 Thread.sleep 或 BlockingQueue.put 等,有两种
实用策略可用于处理 InterruptedException :
-------- 传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为可中断
的阻塞方法。
补充: 传递 InterruptedException 与将 InterruptedException 添加到 throws 子句中一样
容易。
-------- 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。
@@@ 保存中断请求:一种标准的方法就是通过再次调用 interrupt 来恢复中断状态。你不能屏蔽
InterruptedException 。
@@@ 只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该
屏蔽中断请求。
@@@ 通常,可中断的方法会在阻塞或进行重要的工作前首先检查中断,从而尽快地响应中断。
@@@ 如果代码不会调用可中断的阻塞方法,那么仍然可以通过在任务代码中轮询当前线程的中断
状态来响应中断。要选择合适的轮询频率,就需要在效率和响应性之间进行权衡。如果响应性要求
较高,那么不应该调用那些执行时间较长并且不响应中断的方法,从而对可调用的库代码进行一些限制。
### 示例:计时运行
### 通过 Future 来实现取消
@@@ 我们已经使用了一种抽象机制来管理任务的生命周期,处理异常,以及实现取消,即 Future 。
通常,使用现有库中的类比自行编写更好,因此我们将继续使用 Future 和任务框架来构建 timedRun 。
@@@ ExecutorService.submit 将返回一个 Future 来描述任务。
@@@ Future 拥有一个 cancel 方法 , 该方法带有一个 boolean 类型的参数 mayInterruptIfRunning
表示取消操作是否成功(这只是表示任务是否能够接收中断,而不是表示任务是否能检测并处理中断)。
-------- 如果 mayInterruptIfRunning 为 true 并且任务正在某个线程中运行,那么这个线程能被中断。
-------- 如果 mayInterruptIfRunning 为 false 那么意味着 “ 若任务还没有启动 , 就不要运行它 ” ,这种
方式应该用于那些不处理中断的任务中。
@@@ 除非你清楚线程的中断策略,否则不要中断线程,那么在什么情况下调用 cancel 可以将参数
指定为 true ?
------------ 执行任务的线程是由标准的 Executor 创建的, 它实现了一种中断策略使得任务可以通过
中断被取消,所以如果任务在标准的 Executor 中运行,并通过他们的 Future 来取消任务,那么
可以设置 mayInterruptIfRunning 。
------------ 当尝试取消某个任务时,不宜直接中断线程池,因为你并不知道当中断请求达到时正在运行
什么任务--------只能通过任务的 Future 来实现取消。
@@@ 一种良好的编程习惯:取消那些不再需要结果的任务。
@@@ 当 Future.get 抛出 InterruptedException 或 TimeoutException 时,如果你知道不再需要结果,
那么就可以调用 Future.cancel 来取消任务。
### 处理不可中断的阻塞
@@@ 在 Java 库中,许多可阻塞的方法都是通过提前返回或者抛出 InterruptedException 来响应
中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞方法或者
阻塞机制都能响应中断;如果一个线程由于执行同步的 Socket I / O 或者等待获得内置锁而阻塞,那么
中断请求只能设置线程的中断状态,除此之外没有其他任何作用。
@@@ 对于那些由于执行不可中断操作而被阻塞的进程,可以使用类似于中断的手段来停止这些线程,
但这要求我们必须知道线程被阻塞的原因。
@@@ Java.io 包中的同步 Socket I / O
--------- 在服务器应用程序中,最常见的阻塞 I / O 形式就是对套接字进行读取和写入。
@@@ Java.io 包中的同步 I / O
---------- 当中断一个正在 InterruptibleChannel 上等待的线程时,将抛出 ClosedByInterruptException
并关闭链路
----------- 当关闭一个 InterruptibleChannel 时,将导致所有在链路操作上阻塞的线程都抛出
AsynchronousCloseException 。
@@@ Selector 的异步 I / O
----------- 如果一个线程在调用 Selector.select 方法(在 java.nio.channels中)时阻塞了,那么
调用 close 或 wakeup 方法会使线程抛出 ClosedSelectorException 并提前返回。
@@@ 获取某个锁
----------- 如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定
会获得锁,所以将不会理会中断请求。但是,在 Lock 类中提供了 lockInterruptibly 方法,该方法
允许在等待一个锁的同时仍能响应中断。
### 采用 newTaskFor 来封装非标准的取消
@@@ newTaskFor 方法可以封装非标准取消的任务,这是 Java 6 在 ThreadPoolExecutor 中
新增的功能。
---------- newTaskFor 是一个工厂方法,它将创建 Future 来代表任务。
---------- newTaskFor 能返回一个 RunnableFuture 接口 , 该接口扩展了Future 和 Runnable
(并由 FutureTask 实现)
@@@ 通过定制表示任务的 Future 可以改变 Future.cancel 的行为。例如,定制的取消代码可以
实现日志记录或者收集取消操作的统计信息,以及取消一些不响应中断的操作。
》》停止基于线程的服务
@@@ 应用程序通常会创建拥有多个线程的服务,例如线程池,并且这些服务的生命周期通常比
创建它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束。
由于无法通过抢占式的方法来停止线程,因此它们需要自行结束。
@@@ 正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。例如,中断线程
或者修改线程的优先级等。
@@@ 线程池是工作者线程的所有者,如果要中断线程,那么应该使用线程池。
@@@ 与其他封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以
拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。
服务应该提供生命周期方法来关闭它自己以及它所拥有的线程。
@@@ 对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该
提供生命周期。
### 示例:日志服务
@@@ 大多数服务器应用程序都会用到日志。
@@@ 内联日志功能会给一些高容量应用程序带来一定的性能开销。另外一种替代方法是通过
调用 log 方法 将日志消息放入某个队列中,并由其他线程来处理。
@@@ 使日志消息的提交操作成为原子操作。
### 关闭 ExecutorService
@@@ 简单的程序可以直接在 main 函数中启动和关闭全局的 ExecutorService 。而在复杂程序
中,通常会将 ExecutorService 封装在某个更高级别的服务中,并且该服务能提供其自己的生命
周期方法 。
### "毒丸"对象
@@@ 另一种关闭生产者---消费者服务的方式就是使用 “ 毒丸对象”:“ 毒丸 ” 是指一个放在队列
上的对象,其含义是:“ 当得到这个对象时, 立即停止 ”
在 FIFO (先进先出)队列中,“ 毒丸 ” 对象将确保消费者在关闭之前首先完成队列中的
所有工作,在提交 “ 毒丸 ” 对象之前提交的所有工作都会被处理,而生产者在提交了 “ 毒丸 ”对象
后,将不会再提交任何工作。
@@@ 只有在生产者和消费者的数量都已知的情况下,才可以使用 “ 毒丸 ” 对象。
@@@ 只有在无界队列中,“ 毒丸 ” 对象才能可靠地工作。
### 示例:只执行一次的服务
@@@ 如果某个方法需要处理一批任务,并且当所有任务都处理完成后才返回,那么可以通过
一个私有的 Executor 来简化服务的生命周期管理,其中该 Executor 的生命周期是由这个方法
来控制的。(在这种情况下,invokeAll 和 invokeAny 等方法通常会起较大的作用)。
### shutdownNow 的局限性
@@@ 当通过 shutdown 来强行关闭 Executor 时,它会尝试取消正在执行的任务,并返回所有
已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理。
然而,我们无法通过常规方法来找出哪些任务已经开始但尚未结束。
》》处理非正常的线程终止
@@@ 导致线程提前死亡的最主要原因就是 RuntimeException 。由于这些异常表示出现了
某种编程错误或者其他不可修复的错误,因此它们通常不会捕获。它们不会在调用栈中逐层传递,
而是默认地在控制台中输出栈追踪信息,并终止线程。
@@@ 虽然在线程池中丢失一个线程可能会对性能带来一定影响,但如果程序能在包含 50 个
线程的线程池上运行良好,那么在包含 49 个线程的线程池上通常也运行良好。
@@@ 任何代码都可能抛出一个 RuntimeException 。
@@@ 在 Thread API 中提供了 UncaughtExceptionHandler , 它能检测出某个线程由于未
捕获的异常而终结的情况。
@@@ 当一个线程由于未捕获异常而退出时, JVM 会把这个事件报告给应用程序提供的
UncaughtExceptionHandler 异常处理器。如果没有任何异常处理器,那么默认的行为是将栈
追踪信息输出到 System.err 。
@@@ 异常处理器如何处理未捕获的异常,取决于对服务质量的需求。
---------- 最常见的方式是将一个错误信息以及相应的栈追踪信息写入到应用程序日志中。
---------- 还可以采用更直接的响应,例如尝试重新启动线程,关闭应用程序,或者执行修复或诊断
等操作。
@@@ 在运行较长时间的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,
并且该异常处理器至少会将异常信息记录到日志中。
@@@ 要为线程池中的所有线程设置一个 UncaughtExceptionHandler ,需要为 ThreadPoolExecutor
的构造函数提供一个 ThreadFactory 。(与所有的线程操控一样,只有线程的所有者能够改变线程的
UncaughtExceptionHandler )标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个
try-finally 代码块来接收通知,因此当线程结束时,将有新的线程来代替它。
@@@ 只有通过 execute 提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过
submit 提交的任务,无论是抛出的未检查异常还是已检查的异常,都将认为是任务返回状态的一部分。
如果一个由 submit 提交的任务由于抛出了异常而结束,那么这个异常将被 Future.get 封装在
ExecutionException 中重新抛出。
》》JVM 关闭
@@@ JVM 既可以正常关闭,也可以强行关闭。
-------------- 正常关闭的触发方式:
*** 当最后一个“ 正常(非守护)”线程结束时
*** 当调用 System.exit 时
*** 通过其他特定平台的方法关闭时(例如 发送了 SIGINT 信号 或键入 Ctrl-C)
-------------- 强行关闭 JVM
*** 通过调用 Runtime.halt
*** 在操作系统中 “ 杀死 ” JVM 进程
### 关闭钩子
@@@ 在正常关闭中, JVM 首先调用所有已注册的关闭钩子(Shutdown Hook)。
@@@ 关闭钩子是指通过 Runtime.addShutdownHook 注册的但尚未开始的线程。
@@@ JVM 并不能保证关闭钩子的调用顺序。
@@@ 当所有的关闭钩子都执行结束时,如果 runFinalizersOnExit 为 true ,那么 JVM
将运行终结器,然后再停止。
@@@ JVM 并不会在停止或中断任何在关闭时仍然运行的应用程序线程。当 JVM 最终结束
时,这些线程将被强行结束。
@@@ 如果关闭钩子或终结器没有执行完成,那么正常关闭进程 “ 挂起 ” 并且 JVM 必须被
强行关闭。当被强行关闭时,只是关闭 JVM ,而不会运行关闭钩子。
@@@ 关闭钩子应该是线程安全的:它们在访问共享数据时必须使用同步机制,并且小心
地避免发生死锁,这与其他并发代码的要求相同。而且,关闭钩子不应该对应用程序的状态
或者 JVM 的关闭原因作出任何假设,因此在编写关闭钩子的代码时必须考虑周全。最后,
关闭钩子必须尽快退出,因为它们会延迟 JVM 的结束时间,而用户可能希望 JVM 能尽快终止。
@@@ 关闭钩子可以用于实现服务或应用程序的清理工作,例如删除临时文件,或者清除无法
由操作系统自动清除的资源。
@@@ 对所有的服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在
该关闭钩子中执行一系列的关闭操作。这确保了关闭操纵在单个线程中串行执行,从而避免了在关闭
操作之间出现竞争条件或死锁等问题。无论是否使用关闭钩子,都可以使用这项技术,通过将各个
关闭操作串行执行而不是并行执行,可以消除许多潜在的故障。当应用程序需要维护多个服务之间的
显式依赖信息时,这项技术可以确保关闭操作按照正确的顺序执行。
### 守护线程
@@@ 有时候,你希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍 JVM 的关闭。
在这种情况下就需要使用守护线程(Daemon Thread)。
@@@ 线程可以分为两种:普通线程和守护线程。
------------ 在 JVM 启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如
垃圾回收器以及其他执行辅助工作的线程)。
------------ 当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,
主线程创建的所有线程都是普通线程。
@@@ 普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。
----------- 当一个线程退出时, JVM 会检查其他正在运行的线程,如果这些线程都是守护线程,那么
JVM 会正常退出操作。
当 JVM 停止时,所有仍然存在的守护线程都将被抛弃----既不会执行 finally 代码块,也不会
执行回卷栈,而 JVM 只是直接退出。
@@@ 我们应该尽可能少地使用守护线程----很少有操作能够在不进行清理的情况下安全地抛弃。
@@@ 守护线程最好用于执行 “ 内部 ” 任务,例如周期性地从内存的缓存中移除逾期的数据。
@@@ 守护线程通常不能用来替代应用程序管理程序中各个服务的生命周期。
### 终结器
@@@ 当不再需要内存资源时,可以通过垃圾回收器来回收它们,但对于其他一些资源,例如文件
句柄或套接字句柄,当不再需要它们时,必须显式地交给操作系统。为了实现这个功能垃圾回收器对
那些定义了 finalize 方法的对象会进行特殊处理:在回收器释放它们后,调用它们的 finalize 方法,
从而保证一些持久化的资源被释放。
@@@ 在大多数情况下,通常使用 finally 代码块和显式的 close 方法,能够比使用终结器更好地
管理资源。
@@@ 使用终结器的情况:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。
@@@ 避免使用终结器。
》》 小结
@@@ 在任务 、 线程 、 服务以及应用程序等模块中的生命周期结束问题,可能会增加它们在设计和
实现时的复杂性。
@@@ Java 提供了一种协作式的中断机制来实现取消操作,但这要依赖于如何构建取消操作的协议,
以及能否始终遵循这些协议
@@@ 通过使用 FutureTask 和 Executor 框架,可以帮助我们构建可取消的任务和服务。