6.1 在线程中执行任务
第一步要找出清晰的任务边界。大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的请求为边界。
-6.6.1 串行地执行任务
最简单的任务调度策略是在单个线程中串行地执行各项任务。
虽然简单明了,但是每次只能处理一个请求。当服务器正在处理请求时,新到来的连接必须等待直到请求处理完成,然后服务器将再次调用accept。
-6.1.2 显示地为任务创建线程
-6.1.3 无限制创建线程的不足
(1) 线程生命周期的开销非常高
(2) 资源消耗
(3) 稳定性: 在可创建线程的数量上存在一个限制,这个限制值随着平台的不同而不同并受到多个因素制约,包括JVM启动参数,Thread构造函数中请求的栈大小,以及底层操作系统对线程的限制等。
6.2 Executor框架
java.util.concurrent提供了一种灵活的线程池作为Executor框架的一部分。
Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。
-6.2.1 示例:基于Executor的web服务器
通过使用Executor,将请求任务的提交与任务的实际执行解耦,并且只需采用另一种不同的Executor实现,就可以轻松改变服务器的行为:
-6.6.2 执行策略
-6.2.3 线程池
通过调用Executor中的静态工厂方法来创建一个线程池:
-6.2.4 Executor生命周期
虽然知道如何创建一个Executor,但现在却不知道如何关闭它。JVM只有在所有的非守护线程全部终止后才会退出。因此,如果无法正确地关闭Executor,那么JVM将无法结束。Executor扩展了ExecutorService接口,添加了一些用于生命周期管理方法:
-6.2.5 延迟任务与周期任务
Timer类负责管理延迟任务以及周期任务,但是存在一些缺陷,因此考虑使用ScheduledThreadPoolExecutor来代替它。
ps:这里吐槽下这本书的汉化版,102页的图顺序贴错了,汉化版的质量实在是不敢恭维。
6.3 找出可利用的并行性
若使用Executor,必须将任务表述为一个Runnable。 有时候任务边界并非显而易见的。在单个用户请求中仍可能存在可发掘的并行性。
-6.3.1 示例:串行的页面渲染器
先绘制文本元素,同时为图像预留出矩形占位空间,在处理完了第一篇文本后,程序再开始下载图像,并将它们绘制到相应的占位空间中:
我们可以就将问题分解为多个独立的任务并发执行,从而获得更高的CPU利用率和响应灵敏度。
-6.3.2 携带结果的任务Callable和Future
ExecutorService中的所有方法都将返回一个Future,从而将一个Runnable或Callable提交给Executor,并得到一个Future来获得任务的执行结果或取消任务。
-6.3.3 示例:使用Future实现页面渲染器
为了使页面渲染器实现更高的并发性,首先将渲染任务分解成两个任务:一个是渲染所有的文本,另一个是下载所有图像:
-6.3.4 在异构任务并行中存在的局限
-6.3.5 CompletionService:Executor与BlockingQueue
CompletionService将Executor和BlockingQueue的功能融合在一起,可已将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll方法来获得以完成的结果,这些结果在完成时将被封装成Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。CompletionService和ExecutorCompletionService详解
-6.3.6 示例:使用CompletionService实现页面渲染器
为每一幅图像的下载都创建一个独立的任务,并在线程池中执行它们。此外,通过从CompletionService中获取结果以及使每张图片在下载完成后立刻显示出来,能使用户获得一个更加动态和响应的用户界面:
-6.3.7 为任务设置时限
-6.3.8 示例:旅行预定门户网站
小结: