》》在线程中执行任务
@@@ 大多数并发应用程序都是围绕 “ 任务执行 (Task Execution)” 来构造的: 任务通常
是一些抽象的且离散的工作单元。通过把应用程序的工作分解到多个任务中,可以简化程序的组
织结构,提供一种自然的事务边界来优化错误恢复过程,以及提供一种自然的并行工作结构来提
升并发性。
@@@ 当围绕 “ 任务执行 ” 来设计应用程序结构时,第一步就是要找出清晰的任务边界。
@@@ 为了在调度与负载均衡等过程中实现更高的灵活性,每项任务还应该表示应用程序的一
小部分处理能力。
@@@ 大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边
界。
----------- Web 服务器 、 邮件服务器 、 文件服务器 、 EJB 服务器以及数据库服务器等,这些
服务器都能通过网络接受远程客户的连接请求。
----------- 将独立的请求作为任务边界,既可以实现任务的独立性,又可以实现合理的任务规模。
### 串行地执行任务
@@@ 在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。
### 显式地为任务创建线程
@@@ 通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。
@@@ 在正常负载情况下,“ 为每个任务分配一个线程 ” 的方法能提升串行执行的性能。只要请求
的到达速率不超出服务器的请求处理能力,那么这种方法可以同时带来更快的响应性和更高的吞吐率。
### 无限制创建线程的不足
-------- 线程生命周期的开销非常高
线程的创建和销毁需要 JVM 和操作系统提供一些辅助操作。如果请求的到达率非常高且请求的
处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为每个请求创建一个新线程
将消耗大量的计算资源。
-------- 资源消耗
活跃的线程会消耗系统资源,尤其是内存。
如果你已经拥有足够多的线程使所有 CPU 保持忙碌状态,那么再创建更多的线程反而会降低
性能。
-------- 稳定性
在可创建线程的数量上存在一个限制。
@@@ 在一定范围内,增加线程可以提供系统的吞吐率,但如果超出了这个范围,再创建更多的线程
只会降低程序的执行速度,并且如果过多地创建一个线程,那么整个应用程序将崩溃。
@@@ “ 为每个任务分配一个线程 ” 这种方法的问题在于,它没有限制可创建线程的数量,只限制了
远程用户提交 HTTP 请求的速率。与其他的并发危险一样,在原型设计和开发阶段,无限制地创建线
程或许还能较好地运行,但在应用程序部署后并处于高负载下运行时,才会有问题不断地暴露出来。
》》Executor 框架
@@@ 任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。
@@@ java.util.concurrent 提供了一种灵活的线程池实现作为 Executor 框架的一部分。
@@@ 在Java 类库中,任务执行的主要抽象不是 Thread , 而是 Executor 。
@@@ Executor :
-------------- 提供了一种标准的方法将任务的提交过程和执行过程解耦开来,并用 Runnable
来表示任务。
--------------- 提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等
机制。
@@@ Executor 基于生产者--消费者模式,提交任务的线程相当于生产者(生成待完成的工作单元),
执行任务的线程则相当于消费者(执行完成这些工作单元)。如果要在程序中实现一个生产者---消费者
的设计,那么最简单的方式通常就是使用 Executor 。
### 示例:基于 Executor 的 Web 服务器
@@@ 改变 Executor 实现或配置所带来的影响要远远小于改变任务提交方式带来的影响。通常,
Executor 的配置是一次性的,因此在部署阶段可以完成,而提交任务的代码却会不断地扩散到整个
程序中,增加了修改的难度。
### 执行策略
@@@ 通过将任务的提交与执行解耦开来,从而无须太大的困难就可以为某种类型的任务指定和
修改执行策略。
@@@ 各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算机资源以及对服务质量
需求。
@@@ 通过将任务的提交与任务的执行策略分离开来,有助于部署阶段选择与可用硬件资源最匹配
的执行策略。
@@@ 每当看到下面这种形式的代码时:
new Thread(runnable).start( )
并且你希望获得一种更灵活的执行策略时,请考虑使用 Executor 来代替 Thread 。
### 线程池
@@@ 线程池,是指管理一组同构工作线程的资源池。
@@@ 线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的
任务。
工作者线程(Work Thread)的任务很简单:从工作队列中获取一个任务,执行任务,然后返回
线程池并等待下一个任务。
@@@ “ 在线程池中执行任务 ” 比 “ 为每个任务分配一个线程 ” 优势更多。通过重用现有的线程而不是
创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。
@@@ 通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态,同时还可以
防止多线程相互竞争资源而使应用程序耗尽内存或失败。
@@@ 可以通过调用 Executors 中的静态工厂方法之一来创建一个线程池:
------------- newFixedThreadPool
newFixedThreadPool 将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,
直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的
Exception 而结束,那么线程池会补充一个新的线程)。
-------------- newCachedThreadPool
newCachedThreadPool 将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求
时,那么将回收空闲的线程,而当需要增加时,则可以添加新的线程,线程池的规模不存在任何限制。
------------- newSingleThreadExecutor
newSingleThreadExecutor 是一个单线程的 Executor ,它创建单个工作者线程来执行任务,
如果这个线程异常结束,会创建另一个线程来替代。newSingleThreadExecutor 能确保依照任务在队列
中的顺序来串行执行。(例如 FIFO 、 LIFO 、优先级)
------------ newScheduledThreadPool
newScheduledThreadPool 创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,
类似于 Timer
补充:
newFixedThreadPool 和 newCachedThreadPool 这两个工厂方法返回通用的 ThreadPoolExecutor
示例,这些实例可以直接用来构造专门用途的 executor 。
@@@ 从 “ 为每个任务分配一个线程 ” 策略变成基于线程池的策略,将对应用程序的稳定性产生重大的
影响: Web 服务器不会再在高负载情况下失败。
@@@ 通过使用 Executor ,可以实现各种调优 、 管理 、 监视 、 记录日志 、 错误报告和其他功能,
如果不使用任务执行框架,那么要增加这些功能是非常困难的。
### Executor 的生命周期
@@@ Executor 的实现通常会创建线程来执行任务。但 JVM 只有在所有(非守护)线程全部终止后
才会退出。因此,如果无法正确地关闭 Executor ,那么 JVM 将无法结束。
@@@ 为了解决执行服务的生命周期问题, Executor 扩展了 ExecutorService 接口,添加一些用于
生命周期管理的方法(同时还有一些用于任务提交的便利方法)。
-------------- ExecutorService 的生命周期有 3 种状态:运行 、 关闭和已终止。
-------------- ExecutorService 在初始创建时处于运行状态。
-------------- showdown 方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行
完成----包括那些还未执行的任务。
-------------- showdownNow 方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再
启动队列中尚未执行的任务。
-------------- ExecutorService 关闭后提交的任务将由 “ 拒绝执行处理器(Rejected Execution Hanlder)”
来处理,它会抛弃任务,或者使得 execute 方法抛出一个未检查的
RejectedExecutionException 。
-------------- 等所有任务都完成后,ExecutorService 将进入终止状态。
-------------- 可以调用 awaitTermination 来等待 ExecutorService 到达终止状态,或者通过调用
isTerminated 来轮询 ExecutorService 是否已经终止。
--------------- 通常在调用 awaitTermination 之后会立即调用 shutdown , 从而产生同步地关闭
ExecutorService 的效果。
### 延迟任务和周期任务
@@@ Timer 类负责管理延迟任务以及周期任务。然而,Timer 存在一些缺陷。因此,考虑使用
ScheduledThreadPoolExecutor 来代替它。
@@@ 可以通过 ScheduledThreadPoolExecutor 的构造函数或者 newScheduledThreadPool
工厂方法来创建该类的对象。
补充:
----------- Timer 类支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟
变化很敏感,而 ScheduledThreadPoolExecutor 只支持基于相对时间调度。
@@@ Timer 在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么
将破坏其他 TimerTask 的定时精确性。
线程池可以提供多个线程来执行延时任务和周期任务。
@@@ 如果 TimerTask 抛出了一个未检查的异常,那么 Timer 将表现出糟糕的行为。 Timer
线程并不捕获异常,因此当 TimerTask 抛出未检查的异常时将终止定时线程。这种情况下, Timer
也不会恢复线程的执行,而是会错误地认为整个 Timer 都被取消了。因此,已经被调度但尚未
执行的 TimerTask 将不会再执行,新的任务也不能被调度。(这个称之为 “ 线程泄露 ”【Thread
Leakage】) 。
ScheduledThreadPoolExecutor 能正确处理上面表现出错误任务的任务。在 Java 5.0 或
更高的 JDK 中,将很少使用 Timer 。
@@@ 如果要构建自己的调度服务,那么可以使用 DelayQueue ,它实现了 BlockingQueue ,
并为 ScheduledThreadPoolExecutor 提供调度服务。
----------- DelayQueue 管理着一组 Delayed 对象。
----------- 每个 Delayed 对象都有一个相应的延迟时间:在 DelayQueue 中,只有某个元素逾期后,
才能从 DelayQueue 中执行 take 操纵。从 DelayQueue 中返回的对象将根据它们的延迟
时间进行排序。
》》找出可利用的并行性
@@@ Executor 框架帮助指定执行策略,但如果要使用 Executor , 必须将任务表述为一个
Runnable 。
@@@ 即使是服务器应用程序,在单个客户请求中仍可能存在可发掘的并行性,例如数据库服务器。
### 示例:串行的页面渲染器
### 携带结果的任务 Callable 与 Future
@@@ Executor 框架使用 Runnable 作为其基本的任务表示形式。 Runnable 是一种有很大局限的
抽象,虽然 run 能写入到日志文件或者将结果放入某个共享的数据结构,但它不能返回一个值或抛出
一个受检查的异常。
@@@ 许多任务实际上都是存在延迟的计算-----执行数据库查询,从网络上获取资源,或者计算某个复杂
的功能。对于这些任务, Callable 是一种更好的抽象:它认为主入口(即 call)将返回一个值,并可能
抛出一个异常。在 Executor 中包含了一些辅助方法能将其他类型的任务封装为一个 Callable ,例如
Runnable 和 java.security.PrivilegedAction 。
@@@ Runnable 和 Callable 描述的都是抽象的计算任务。这些任务通常是有范围的,即都有一个
明确的起始点,并且最终会结束。
@@@ Executor 执行的任务有 4 个生命周期阶段:创建 、 提交 、 开始 和完成。由于有些任务可能要
执行很长的时间,因此通常希望能够取消这些任务。
在 Executor 框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有
当它们能相应中断时,才能取消。取消一个已经完成的任务不会有任何影响。
@@@ Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取
任务的结果和取消任务等。
在 Future 规范中包含的隐含意义是,任务的生命周期只能前进,不能后退,就像 ExecutorService
的生命周期一样。当某个任务完成后,它将永远停留在 “ 完成 ”状态上。
@@@ get 方法取决于任务的状态(尚未开始 、 正在运行 、 已完成)。
-------- 如果任务已经完成,那么 get 会立即返回或者抛出一个异常 Exception
-------- 如果任务没有完成,那么 get 将阻塞并直到任务完成
-------- 如果任务抛出了异常,那么 get 将该异常封装为 ExecutionException 并重新抛出
-------- 如果任务被取消,那么 get 将抛出 CancellationException 。
-------- 如果 get 抛出了 ExecutionException , 那么可以通过 getCause 来获得被封装的
初始异常。
@@@ 在将 Runnable 或 Callable 提交到 Executor 的过程中,包含了一个安全发布过程,
即将 Runnable 或 Callable 从提交线程发布到最终执行任务的线程。类似地,在设置 Future
结果的过程中也包含了一个安全发布,即将这个结果从计算它的线程发布到任何通过 get
获得它的线程。
### 示例:使用 Future 实现页面渲染器
@@@ Callable 和 Future 有助于表示这些协同任务之间的交互。
@@@ get 方法拥有 “ 状态依赖 ” 的内在特性,因而调用者不需要知道任务的状态,此外在
任务提交和获得结果中包含的安全发布属性也确保了这个方法是线程安全的。 Future.get 的
异常处理代码将处理两个可能的问题:任务遇到一个 Exception ,或者调用 get 的线程在获得
结果之前被中断。
### 在异构任务并行化中存在的局限
@@@ 只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载
分配到多个任务中带来的真正性能提升。
### CompletionService : Executor 与 BlockingQueue
@@@ 完成服务 ( CompletionService):
----------- CompletionService 将 Executor 和 BlockingQueue 的功能融合在一起。你可以将
Callable 任务提交给它来执行,然后使用类似于队列操作的 take 和 poll 等方法来
获得已完成的结果,而这些结果在完成时将被封装为 Future 。
------------ ExecutorCompletionService 实现了 CompletionService ,并将计算部分委托给一个
Executor 。
### 示例:使用 CompletionService 实现页面渲染器
@@@ CompletionService 的作用就相当于一组计算的句柄。
通过记录提交给 CompletionService 的任务数量,并计算出已经获得的已完成结果的数量,
即使使用一个共享的 Executor ,也能知道已经获得了所有任务结果的时间。
### 为任务设置时限
@@@ 一个门户网站可以从多个数据源并行地获取数据,但可能只会在指定的时间内等待数据,
如果超出了等待时间,那么只显示已经获得的数据。
@@@ 在支持时间限制的 Future.get 中支持:当结果可用时,它将立即返回,如果在指定时限内
没有计算出结果,那么将抛出 TimeoutException 。
@@@ 在使用限时任务需要注意,当这些任务超时后应该立即停止,从而避免为继续计算一个不再
使用的结果而浪费计算资源。要实现这个功能,可以由任务本身来管理它的限定时间,并且在超时后
终止执行或取消任务。此时可再次使用 Future , 如果一个限时的 get 方法 抛出了 TimeoutException
那么可以通过 Future 来取消任务。如果编写的任务是可取消的,那么可以提前终止它,以免消耗过多
的资源。
### 示例:旅行预订门户网站
》》小结:
@@@ 通过围绕任务执行来设计应用程序,可以简化开发过程,并有助于实现并发。
@@@ Executor 框架将任务提交和执行策略解耦开来,同时还支持多种不同类型的执行策略。
@@@ 当需要创建线程来执行任务时,可以考虑使用 Executor 。
@@@ 要想在将应用程序分解为不同的任务时获得最大的好处,必须定义清晰的任务边界。
某些应用程序中存在着比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的
并行性。