Executor 框架帮助指定执行策略,但如果要使用Executor,必须将任务表述为一个Runñable。在大多数服务器应用程序中都存在一个明显的任务边界:单个客户请求。但有时候,任务边界并非是显而易见的,例如在很多桌面应用程序中。即使是服务器应用程序,在单个客户请求中仍可能存在可发掘的并行性,例如数据库服务器。(请参见[CPJ4.4.1.1]了解在选择任务边界时的各种权衡因素及相关讨论。)
SECONDS. sleep(5);timer. schedule(new ThrowTask(),1);
}
static class ThrowTask extends TimerTask {
public void run(){throw new RuntimeException();}
}
}
本节中我们将开发一些不同版本的组件,并且每个版本都实现了不同程度的并发性。该示例组件实现浏览器程序中的页面渲染(Page-Rendering)功能,它的作用是将HTML 页面绘制到图像缓存中。为了简便,假设HTML 页面只包含标签文本,以及预定义大小的图片和URL。.
示例:串行的页面渲染器
最简单的方法就是对HTML 文档进行串行处理。当遇到文本标签时,将其绘制到图像缓存中。当遇到图像引用时,先通过网络获取它,然后再将其绘制到图像缓存中。这很容易实现,程序只需将输入中的每个元素处理一次(甚至不需要缓存文档),但这种方法可能会令用户感到烦恼,他们必须等待很长时间,直到显示所有的文本。
另一种串行执行方法更好一些,它先绘制文本元素,同时为图像预留出矩形的占位空间,在处理完
清单6-10的SingleThreadRenderer中给出了这种方法。
图像下载过程的大部分时间都是在等待I/O操作执行完成,在这期间CPU 几乎不做任何工作。因此,这种串行执行方法没有充分地利用CPU,使得用户在看到最终页面之前要等待过长的时间。通过将问题分解为多个独立的任务并发执行,能够获得更高的CPU利用率和响应灵敏度。
for (ImageData data :imageData)imageData. add(imageInfo. downloadImage());
renderImage(data);
}
}
携带结果的任务Callable与Future
Executor 框架使用Runnable 作为其基本的任务表示形式。Runnable 是一种有很大局限的抽象,虽然run 能写入到日志文件或者将结果放入某个共享的数据结构,但它不能返回一个值或抛出一个受检查的异常。
许多任务实际上都是存在延迟的计算——执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务, Callable 是一种更好的抽象:它认为主入口点(即call)将返回一个值,并可能抛出一个异常。在Executor中包含了一些辅助方法能将其他类型的任务封装为一个Callable,例如Runnable 和java. sec"rity. PrivilegedAction。.
Runnable 和Callable 描述的都是抽象的计算任务。这些任务通常是有范围的,即都有一个明确的起始点,并且最终会结束。Executor 执行的任务有4个生命周期阶段:创建、提交、开始和完成。由于有些任务可能要执行很长的时间,因此通常希望能够取消这些任务。在Executor 框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们能响应中断时,才能取消。取消一个已经完成的任务不会有任何影响。(第7章将进一步介绍取消操作。)
Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在程序清单6-11 中给出了Callable 和Future。在Future规范中包含的隐含意义是,任务的生命周期只能前进,不能后退,就像ExecutorService 的生命周期一样。.当某个任务完成后,它就永远停留在“完成”状态上。
get 方法的行为取决于任务的状态(尚未开始、正在运行、已完成)。如果任务已经完成,
要使用Callable 来表示无返回值的任务,可使用Callable<Void>。
那么get会立即返回或者抛出一个Exception,如果任务没有完成,那么get 将阻塞并直到任务完成。如果任务抛出了异常,那么get 将该异常封装为ExecutionException并重新抛出。如果任务被取消,那么get 将抛出CancellationException。如果get 抛出了ExecutionException,那么可以通过getCause来获得被封装的初始异常。
程序清单6-11 Callable 与 Future 接口
public interface Callable<V>{
V call() throws Exception;
}
public interface Future<V>{
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException,ExecutionException,
CancellationException;
V get (long timeout,TimeUnit unit)
throws InterruptedException,ExecutionException,
CancellationException,TimeoutException;
}
可以通过许多种方法创建一个Future来描述任务。ExecutorService中的所有submit方法都将返回一个Future,从而将一个Runnable或Callable 提交给Executor,并得到一个Future用来获得任务的执行结果或者取消任务。还可以显式地为某个指定的Runnable 或Callable 实例化一个FutureTask。(由于FutureTask实现了Runnable,因此可以将它提交给Executor 来执行,或者直接调用它的run 方法。)
从Java 6 开始,ExecutorService实现可以改写AbstractExecutorService中的newTaskFor 方法,从而根据已提交的Runnable 或Callable来控制Future的实例化过程。在默认实现中仅创建了一个新的FutureTask,如程序清单6-12所示。
程序清单6-12 ThreadPoolExecutor中newTaskFor的默认实现
protected <T>RunnableFuture<T>newTaskFor(Callable<T>task){
return new FutureTask<T>(task);
)
在将Runnable 或Callable 提交到Executor的过程中,包含了一个安全发布过程(请参见3.5节),即将Runnable或Callable 从提交线程发布到最终执行任务的线程。类似地,在设置Future结果的过程中也包含了一个安全发布,即将这个结果从计算它的线程发布到任何通过get 获得它的线程。
示例:使用Future实现页面渲染器
为了使页面渲染器实现更高的并发性,首先将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图像。(因为其中一个任务是CPU密集型,而另一个任务是I/O密集型,因此这种方法即使在单CPU系统上也能提升性能。)
Callable 和Future 有助于表示这些协同任务之间的交互。在程序清单6-13 的Future-Renderer 中创建了一个Callable来下载所有的图像,并将其提交到一个ExecutorService。这将返回一个描述任务执行情况的Future。当主任务需要图像时,它会等待Future. get的调用结果。如果幸运的话,当开始请求时所有图像就已经下载完成了,即使没有,至少图像的下载任务也已经提前开始了。
Callable<List<ImageData>>task=
new Callable<List<ImageData>>(){
public List<ImageData>call(){
List<ImageData>result
=new ArrayList<ImageData>();
for (ImageInfo imageInfo:imageInfos)
result. add(imageInfo. downloadImage());
return result;
}
} ;
Future<List<ImageData>>future =executor. submit(task);
renderText(source);
try {
List<ImageData>imageData =future. get();
for (ImageData data :imageData)
renderImage(data);
}catch (InterruptedException e){
// 重新设置线程的中断状态
Thread. currentThread(). interrupt();
// 由于不需要结果,因此取消任务
future. cancel(true);
}catch (ExecutionException e){
throw launderThrowable(e. getCause());
}
}
}
get方法拥有“状态依赖”的内在特性,因而调用者不需要知道任务的状态,此外在任务提交和获得结果中包含的安全发布属性也确保了这个方法是线程安全的。Future. get 的异常处理代码将处理两个可能的问题:任务遇到一个Exception,或者调用get的线程在获得结果之前被中断
FutureRenderer 使得渲染文本任务与下载图像数据的任务并发地执行。当所有图像下载完后,会显示到页面上。这将提升用户体验,不仅使用户更快地看到结果,还有效利用了并行性,但我们还可以做得更好。用户不必等到所有的图像都下载完成,而希望看到每当下载完一幅图像时就立即显示出来。
在异构任务并行化中存在的局限
在上个示例中,我们尝试并行地执行两个不同类型的任务——下载图像与渲染页面。然而,通过对异构任务进行并行化来获得重大的性能提升是很困难的。
两个人可以很好地分担洗碗的工作:其中一个人负责清洗,而另一个人负责烘干。然而,要将不同类型的任务平均分配给每个工人却并不容易。当人数增加时,如何确保他们能帮忙而不是妨碍其他人工作,或者在重新分配工作时,并不是容易的事情。如果没有在相似的任务之间找出细粒度的并行性,那么这种方法带来的好处将减少。
当在多个工人之间分配异构的任务时,还有一个问题就是各个任务的大小可能完全不同。如果将两个任务A和B分配给两个工人,但A的执行时间是B的10倍,那么整个过程也只能加速9%。最后,当在多个工人之间分解任务时,还需要一定的任务协调开销:为了使任务分解能提高性能,这种开销不能高于并行性实现的提升。
FutureRenderer使用了两个任务,其中一个负责渲染文本,另一个负责下载图像。如果渲染文本的速度远远高于下载图像的速度(可能性很大),那么程序的最终性能与串行执行时的性能差别不大,而代码却变得更复杂了。当使用两个线程时,至多能将速度提高一倍。因此,虽然做了许多工作来并发执行异构任务以提高并发度,但从中获得的并发性却是十分有限的。(在11.4.2 节和11.4.3 节中的示例说明了同一个问题。)
只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。
CompletionService;Executor与BlockingQueue
如果向Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后反复使用get 方法,同时将参数timeout指定为0,从而通过轮询来判断任务是否完成。这种方法虽然可行,但却有些繁琐。幸运的是,还有一种更好的方法:完成服务(CompletionService)。
CompletionService 将Executor 和BlockingQueue 的功能融合在一起。你可以将Callable 任务提交给它来执行,然后使用类似于队列操作的take 和poll等方法来获得已完成的结果,而这些结果会在完成时将被封装为Future。ExecutorCompletionService 实现了CompletionService,并将计算部分委托给一个Executor。
ExecutorCompletionService 的实现非常简单。在构造函数中创建一个BlockingQueue来保存计算完成的结果。当计算完成时,调用Future-Task中的done方法。当提交某个任务时,该任务将首先包装为一个QueueingFuture,这是FutureTask的一个子类,然后再改写子类的done 方法,并将结果放入BlockingQueue中,如程序清单6-14所示。take 和poll 方法委托给了BlockingQueue,这些方法会在得出结果之前阻塞。
private class QueueingFuture<V>extends FutureTask<V>{
QueueingFuture(Callable<V>c){super(c);}
QueueingFuture(Runnable t,v r){super(t,r);}
protected void done(){
completionQueue. add(this);
}
}
示例:使用CompletionService实现页面渲染器
可以通过CompletionService从两个方面来提高页面渲染器的性能:缩短总运行时间以及提高响应性。为每一幅图像的下载都创建一个独立任务,并在线程池中执行它们,从而将串行的下载过程转换为并行的过程:这将减少下载所有图像的总时间。此外,通过从CompletionService 中获取结果以及使每张图片在下载完成后立刻显示出来,能使用户获得一个更加动态和更高响应性的用户界面。如程序清单6-15的Renderer 所示。
public class Renderer {
private final ExecutorService executor;
Renderer (ExecutorService executor){this. executor =executor;}
void renderPage(CharSequence source){
List<ImageInfo>info =scanForImageInfo(source);
CompletionService<ImageData>completionService =
new ExecutorCompletionService<ImageData>(executor);
for (final ImageInfo imageInfo:info)
completionService. submit(new Callable<ImageData>(){
public ImageData ca11(){
return imageInfo. downloadImage();
renderText(source);
try {
for ( int t = 0,n = info. size( );t < n;t++) {
Future<ImageData>f =completionService. take();
ImageData imageData =f. get();
renderImage(imageData);
}
} catch (InterruptedException e){
Thread. current Thread(). interrupt();
}catch (ExecutionException e){
throw launderThrowable(e. getCause());
}
}
}
多个ExecutorCompletionService 可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个公共Executor 的ExecutorCompletionService。因此,CompletionService的作用就相当于一组计算的句柄,这与Future 作为单个计算的句柄是非常类似的。通过记录提交给CompletionService 的任务数量,并计算出已经获得的已完成结果的数量,即使使用一个共享的Executor,也能知道已经获得了所有任务结果的时间。
为任务设置时限
有时候,如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。例如,某个Web应用程序从外部的广告服务器上获取广告信息,但如果该应用程序在两秒钟内得不到响应,那么将显示一个默认的广告,这样即使不能获得广告信息,也不会降低站点的响应性能。类似地,一个门户网站可以从多个数据源并行地获取数据,但可能只会在指定的时间内等待数据,如果超出了等待时间,那么只显示已经获得的数据。
在有限时间内执行任务的主要困难在于,要确保得到答案的时间不会超过限定的时间,或者在限定的时间内无法获得答案。在支持时间限制的Future. get 中支持这种需求:当结果可用时,它将立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。
在使用限时任务时需要注意,当这些任务超时后应该立即停止,从而避免为继续计算一个不再使用的结果而浪费计算资源。要实现这个功能,可以由任务本身来管理它的限定时间,并且在超时后中止执行或取消任务。此时可再次使用Future,如果一个限时的get 方法抛出了TimeoutException,那么可以通过Future 来取消任务。如果编写的任务是可取消的(参见第7章),那么可以提前中止它,以免消耗过多的资源。在程序清单6-13和6-16的代码中使用了这项技术。
程序清单6-16 给出了限时Future. get的一种典型应用。在它生成的页面中包括响应用户请求的内容以及从广告服务器上获得的广告。它将获取广告的任务提交给一个Executor,然后计算剩余的文本页面内容,最后等待广告信息,直到超出指定的时间。如果get超时,那么将取消广广告获取任务,并转而使用默认的广告信息。
Page renderPageWithAd() throws InterruptedException {
long endNanos= System.nanoTime( )+TIME BUDGET;
Future<Ad>f =exec. submit(new FetchAdTask());
//在等待广告的同时显示页面
Page page =renderPageBody();
Ad ad;
try {
// 只等待指定的时间长度
long timeLeft =endNanos -System. nan òTime();
传递给get 的timeout 参数的计算方法是,将指定时限减去当前时间。这可能会得到负数,但java. util.concurrent 中所有与时限相关的方法都将负数视为零,因此不需要额外的代码来处理这种情况。
㊂ Future. cancel的参数为true 表示任务线程可以在运行过程中中断。。
ad =f. get(timeLeft,NANOSECONDS);
}catch (ExecutionException e){
ad=DEFAULT_AD;
}catch (TimeoutException e){
ad=DEFAULT_AD;
f. cancel(true);
}
page. setAd(ad);
return page;
}
示例:旅行预定门户网站
“预定时间”方法可以很容易地扩展到任意数量的任务上。考虑这样一个旅行预定门户网站:用户输入旅行的日期和其他要求,门户网站获取并显示来自多条航线、旅店或汽车租赁公司的报价。在获取不同公司报价的过程中,可能会调用Web服务、访问数据库、执行一个EDI 事务或其他机制。在这种情况下,不宜让页面的响应时间受限于最慢的响应时间,而应该只显示在指定时间内收到的信息。对于没有及时响应的服务提供者,页面可以忽略它们,或者显示一个提示信息,例如“Did not hear from Air Java in time。”
从一个公司获得报价的过程与从其他公司获得报价的过程无关,因此可以将获取报价的过程当成一个任务,从而使获得报价的过程能并发执行。创建n个任务,将其提交到一个线程池,保留n个Future,并使用限时的get 方法通过Future串行地获取每一个结果,这一切都很简单,但还有一个更简单的方法——invokeAll。
程序清单6-17使用了支持限时的invokeAll,将多个任务提交到一个ExecutorService 并获得结果。InvokeAll方法的参数为一组任务,并返回一组Future。这两个集合有着相同的结构。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能将各个Future与其表示的Callable 关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll 将返回。当超过指定时限后,任何还未完成的任务都会取消。当invokeAll 返回后,每个任务要么正常地完成,要么被取消,而客户端代码可以调用get 或isCancelled来判断究竟是何种情况。
private class QuoteTask implements Callable<TravelQuote>{
private final TravelCompany company;
private final TravelInfo travelInfo;
...
public TravelQuote call() throws Exception {
return company. solicitQuote(travelInfo);
}
}
public List<TravelQuote>getRankedTravelQuotes(
TravelInfo travelInfo, Set<TravelCompany>companies,
Comparator<TravelQuote>ranking, long time,TimeUnit unit)
throws InterruptedException {
List<QuoteTask>tasks =new ArrayList<QuoteTask>();
for (TravelCompany company :companies)
tasks. add(new QuoteTask(company,travelInfo));
List<Future<TravelQuote>>futures =
exec. invokeAll(tasks, time, unit);
List<TravelQuote>quotes =
new ArrayList<TravelQuote>(tasks. size());
Iterator<QuoteTask>taskIter =tasks. iterator();
for (Future<TravelQuote>f :futures){
QuoteTask task =taskIter. next();
try {
quotes. add(f. get());
}catch (ExecutionException e){
quotes. add(task. getFailureQuote(e. getCause()));
}catch (CancellationException e){
quotes. add (task. getTimeoutQuote(e));
}
}
Collections. sort(quotes, ranking);
return quotes;
}