任务执行
大部分并发应用程序都是围绕“任务执行”来构造的:任务通常是一些抽象的且离散的工作单元。
一、在线程中执行任务
当围绕“任务执行”来设计应用程序结构时,第一步就是要找出清晰的任务边界。在理想情况下,各个任务之间是相互独立的,独立性有助于实现并发。
在正常的负载下,服务器应用程序应该同时表现出良好的吞吐量和快速的响应性。应用程序提供商希望程序尽可能支持多的用户,从而降低每个用户的服务成本,而用户则是希望获得快的响应。而且,当负荷过载时,应用程序的性能应该是逐渐降低,而不是直接失败。为了实现这些目标,应该选择清晰的任务边界以及明确的任务执行策略。
大部分服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。例如:web服务器、邮件服务器、文件服务器、EJB容器以及数据库服务器等,这些服务器都能通过网络接收远程客户的连接请求。因此将独立的请求作为任务边界,即可实现任务的独立性、又可实现合理的任务规模。
1、任务执行策略之串行地执行任务
最简单的策略就是在单个线程中串行地执行各项任务,在下面的代码中,SingleThreadWebServer通过80端口接收http请求,再串行地处理请求的任务。
class SingleThreadWebServer
{
public static void main(String[] args) throw IOException
{
ServerSocket socket = new ServerSocket(80);
while(true)
{
Socket connection = socket.accept();
handleRequest(connection);
}
}
}
主线程在接收连接与处理相关请求操作之间不断地交替运行,当服务器正在处理请求时,新来的连接必须等待直到前一个请求被处理完成,然后服务器调用accept才能接收这个连接。如果处理请求的速度很快,handleRequest立即返回,那么这种方式还可行,但是现实中的并不是如此。
而且使用串行的方式,服务器的资源利用率很低,因为在执行IO操作的时候,CPU将处于空闲状态。串行处理机制通常都无法提供高吞吐率和快速响应性。
2、任务执行策略之显式为任务创建线程
通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。
class SingleThreadWebServer
{
public static void main(String[] args) throw IOException
{
ServerSocket socket = new ServerSocket(80);
while(true)
{
Socket connection = socket.accept();
handleRequest(connection);
}
}
}
主线程仍然不断的交替执行"接受外部连接"与"分发请求"等操作。和串行处理请求的区别在于,对于每个连接,主循环都将创建一个新线程来处理请求,而不是在主循环中进行处理。由此可得出三个结论:
- 任务处理过程从主线程中分离出来,使得主循环能更快地重新等待下一个到来的连接,这就使得可以在完成前面请求之前可以接受新的请求,从而提高响应性。
- 任务可以并行处理,从而能同时服务多个请求。
- 任务处理代码必须是线程安全的,因为当有多个任务的时候会并发的调用这段代码。
3、无限制创建线程的不足
缺陷:
- 线程生命周期的开销非常高。线程的创建和销毁需要时间,需要JVM和操作系统提供一些辅助操作。如果请求的到达率非常高且请求的处理过程是轻量级,例如大多数服务器的应用程序就是这种情况,那么为每个请求创建一个新线程将消耗大量的计算资源。
- 资源消耗(活跃的线程会消耗系统资源,尤其是内存)。大量的空闲线程会占用许多内存,给垃圾回收器带来压力,大量的线程会在竞争CPU资源时产生其他性能开销。
- 稳定性(在可创建线程的数量上存在一个限制)。这个限制值将随着平台的不同而不同,并且受多个因素制约。
在一定范围内创建线程可以提高吞吐量,但是超过这个范围,再创建更多的线程只会降低程序的执行速度,并且如果过多的创建一个线程,那么整个程序将崩溃。
二、Executor框架
什么是Executor框架
我们知道线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。线程用于执行异步任务,单个的线程既是工作单元也是执行机制,从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,他是一个用于统一创建与运行的接口。Executor框架实现的就是线程池的功能。
executor 英 [ɪɡˈzekjətə®] 执行器
任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。我们已经分析了两种通过线程来执行任务的策略,即把所有任务放在单个线程中串行执行和将每个任务放在各自的线程中执行。串行执行的问题在于其糟糕的响应性和吞吐量。而为每个任务分配一个线程的问题在于资源管理的复杂性。
第五章中有提到,在队列中,如果生产者生成速率比消费者处理速率快,那么会在工作队列中累计,最终耗尽内存。虽然阻塞队列提供了offer方法,如果数据不能被添加到队列中,则返回一个失败的状态,那么能创建更多灵活的策略来处理负荷过载情况。而构建高可靠的应用程序时,有界队列是一种强大的资源管理工具,能避免内存损耗情况。
Executor框架中线程池的实现是由java.util.concurrent提供的。在java类中,任务执行的主要抽象不是Thread而是Executor,如程序所示:
public interface Executor
{
void execute(Runnable command);
}
虽然Executor是一个简单的接口,但是它却为灵活且强大的异步任务执行框架提供了基础,该框架支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable表示任务。
Executor基于生产者–消费者模式,提交任务操作类似生产者,执行任务相当于消费者,如果要在程序中实现一个生产者–消费者的设计,那么最简单的方式就是使用Executor。
1、示例:基于Executor的Web服务器
在Executor中只提供了executor接口,通过该接口可以用来执行以及提交的Runnable任务对象,这个接口将"任务提交"与"任务执行"解耦的方法,将每次的请求作为一个任务。
class TaskExecutionWebServer
{
private static final int NTHERADS = 100;//指定Executor中线程池多少个线程
private static final Executor exec = Executors.newFixedThreadPool(NTHERADS);
public static void main(String[] args) throws IOExeception{
ServerSocket socket = new ServerSocket(80);
while(true)
{
final Socket connection = socket.accept();
Runnable task = new Runnable(){
public void run(){
handleRequest(connection);
}
};
exec.execute(task);
}
}
}
我们很容易将TaskExecutionWebServer修改为类似ThreadPerTaskWebServer的行为,只需使用一个为每个请求都创建新线程的Executor。编写这样的Executor很简单,例如ThreadPerTaskExecutor。
public class ThreadPerTaskExecutor implements Executor
{
public void execute(Runnable r)
{
new Thread(r).start();
};
}
同样还可以编写一个Executor使TaskExecutionWebServer行为类似单线程行为,以同步方式执行每个任务
public class WithinThreadExecutor implements Executor{
public void execute(Runnable r)
{
r.run();
};
}
2、执行策略
通过将任务的提交和执行解耦开来,从而无需太大的困难就可以为某种类型的任务知道和修改执行策略。在执行策略中定义了任务执行的what、where、when、how等方面,包括:
- 在什么(what)线程中执行任务?
- 任务按照什么(what)顺序执行(FIFO、LIFO、优先级)?
- 有多少个(How many)任务能并发执行
- 在队列中有多少个(How many)任务在执行等待
- 若系统由于过载而拒绝一个任务,那么应该选择which任务?how to 通知应用程序有任务被拒绝呢?
3、线程池
线程池是和工作队列(work Queue)密切相关,在工作队列中保存了所有等待执行的任务。线程池中有一个工作者线程(Worker Thread),工作者线程专门从工作队列中获取一个任务,执行任务,然后返回线程池中并等待下一个任务。
“在线程池中执行任务”比“为每个任务分配一个线程”优势更多,
- 线程池可以重用现有的线程,而非是创建一个新线程,避免了线程创建和销毁过程中产生的巨大开销。
- 当请求到达的时候,工作线程已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高响应性。
- 并且通过调节线程池的大小,可以防止多线程相互竞争资源而使应用程序耗尽内存而失败。
Executors接口的实现中有许多静态工厂方法可以创建线程池:
- newFixedThreadPool(创建固定长度的线程池,每当提交一个任务时就会创建一个线程,直到达到线程池的最大数量。)
- newCachedThreadPool(可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加的时候,则可以添加新的线程,线程池的规模不受任何限制。)
- newSingleThreadPool(单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另外一个线程代替。)
- newScheduledThreadPool(固定长度的线程池,而且以延迟或者定时的方式来执行任务,类似于Timer。)
4、Executor的生命周期(ExecutorService)
Executor的实现通常会创建线程来执行任务,但是JVM只有在所有(非守护)线程全部终止后才会退出,因此若无法正确关闭Executor,那么JVM无法结束。
Executor采用异步方式来执行任务,异步意味着在任何时刻,之前提交的任务状态不是立即可见的,有些任务可能已经完成,有些任务可能正在运行。
ExecutorService继承(扩展)了Executor接口,添加了一些用于生命周期管理的方法。
public interface ExecutorService extends Executor{
void shutdown();//平稳的关闭,不再接收新的任务、同时等待已经提交的任务执行
List<Runnable> shutdownNow();//执行暴力的关闭,尝试取消所有运行的任务,并且不再启动队列中尚未启动的任务
boolean isShutDown();//如果此执行程序已关闭,则返回 true
boolean isTerminated();//查询ExecutorService是否已经终止
boolean awaitTermination(long timeout, TimeUint unit) throws InterruptedException;
//通常调用awaitTermination后会立即调用shutdown,产生同步关闭ExecutorServicce
Future<?> submit(Runnable task);
//提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null。 抛出RejectedExecutionException - 如果任务无法安排执行 ,NullPointerException - 如果该任务为 null
.....
}
ExecutorService的生命周期有三种状态:运行、关闭、已终止状态。
方法解释:
- ExecutorService在创建的时候处于运行状态。
- shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务完成(包括还未开始的任务)。
- shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未执行的任务。
- ExecutorService关闭后提交的任务将由“拒绝执行处理器”来处理,她会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态或者调用isTerminated来轮询ExecutorService是否已经终止。通常调用awaitTermination后会立即调用shutdown,产生同步关闭ExecutorServicce的效果。
ExecutorService是一个重要的接口,shutdown、isShutDown、isTerminated、awaitTermination大体功能需要了解。
下面是加入生命周期的web服务器
class LifecycleWebServer
{
private final ExecutorService exec = .....;
public void start() throws IOException{
ServerSocket socket = new ServerSocket(80);
while(!exec.isShutdown())
{
try{
final Socket conn = socket.accept();
exec.execute(new Runnable(){
public void run(){ handleRequest(conn);}
});
}catch(RejectExecutionException e)
{
if(! exec.isShutdown())
{
log("task submission rejected:",e);
}
}
}
}
public void stop(){ exec.shutdown();}
void handleRequest(Socket connection)
{
Request req = readRequest(connection);
if(isShutdownRequest(req))
stop();
else
dispatcherRequest(req);
}
}
5、延迟任务和周期任务
没有线程池前,使用Timer类负载管理延迟任务(如在100ms后执行该任务)、周期任务(每10ms执行一次该任务)。然而Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它。
Timer在执行所有的定时任务时,只会创建一个线程,如果某个任务的执行时间过长,那么将会破坏其他TImerTask的定时精确性。如某个周期TimerTask需要每10ms执行一次,而另外一个TimerTask需要执行40ms,那么这个周期任务或者在40ms任务完成后快速连续执行4次或者彻底丢失4次调用(取决于它是基于固定速率来调度还是基于固定延时来调度)。线程池能弥补这个缺陷,他可以提供多个线程来执行延时任务和周期任务。
同时,Timer有另一个问题就是,如果TimerTask抛出一个未检查的异常,那么Timer线程不会捕获异常,而是终止定时线程,错误的认为整个Timer都被取消了。因此,已经被调度但尚未执行的TimerTask将不会再被执行,新的任务也不能被调度,该问题称之为“线程泄露”。例如OutOfTime,你可能幼稚的认为程序运行6秒后会退出,但是实际上运行1秒后,抛出异常后,第二个任务也不能执行了,ScheduledThreadPoolExecutor能正确处理这些表现出错误行为的任务。
public class OutOfTime
{
public static void main(String[] args) throws Exception{
Timer timer = new Timer();
timer.schedule(new ThrowTask(),1); //schedule 英[ˈʃedjuːl]工作计划; 日程安排; (电视或广播) 节目表; (价格、收费或条款等的) 一览表,明细表,清单
Thread.sleep(1);
timer.schedule(new ThrowTask(),1);
Thread.sleep(5);
}
static class ThrowTask extends TimerTask{
public void run() {throw new RuntimeException();}
}
}
Timer类存在一些缺陷,此时可以考虑使用ScheduledThreadPoolExecutor来替代它。Timer是基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感,而ScheduledThreadPoolExecutor是基于相对时间的调度。要构建自己的调度服务,可以使用DelayQueue,DelayQueue管理一组Delayed对象,每个Delayed对象都有一个相应的延迟时间。
import java.util.Timer;
import java.util.TimerTask;
class Test
{
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
System.out.println("11232");
}
}, 2000 , 2000); //延迟两秒,每两秒会执行一次sout
}
}
这个方法是调度一个task,在delay(ms)后开始调度,每次调度完后,最少等待period(ms)后才开始调度。
public void schedule(TimerTask task, long delay, long period)
调度一个task,在delay(ms)后开始调度,然后每经过period(ms)再次调度,貌似和方法:schedule是一样的,其实不然,后面你会根据源码看到,schedule在计算下一次执行的时间的时候,是通过当前时间(在任务执行前得到) + 时间片,而scheduleAtFixedRate方法是通过当前需要执行的时间(也就是计算出现在应该执行的时间)+ 时间片,前者是运行的实际时间,而后者是理论时间点,例如:schedule时间片是5s,那么理论上会在5、10、15、20这些时间片被调度,但是如果由于某些CPU征用导致未被调度,假如等到第8s才被第一次调度,那么schedule方法计算出来的下一次时间应该是第13s而不是第10s,这样有可能下次就越到20s后而被少调度一次或多次,而scheduleAtFixedRate方法就是每次理论计算出下一次需要调度的时间用以排序,若第8s被调度,那么计算出应该是第10s,所以它距离当前时间是2s,那么再调度队列排序中,会被优先调度,那么就尽量减少漏掉调度的情况。
public void scheduleAtFixedRate(TimerTask task, long delay, long period);
三、找出可利用的并行性
要使用Executor框架,必须将任务表述为Runnable,在大部分服务器应用程序中都存在一个明显的任务边界:如单个客户请求,当然也有一些任务边界并非是显而易见的,需要自己挖掘可能存在的并行性。
1、串行的页面渲染
浏览器程序有页面渲染(Page-Rendering)功能,它的作用是将HTML页面绘制到图像缓存中。为了简单,假设HTML页面只包含标签文本,以及预定大小的图片和URL。最简单的方法就是对HTML文档进行串行处理。当遇到文本标签时,将其绘制到图像缓存中。当遇到图像引用时,想通过网络获取它,然后再绘制到图像缓存中。这很容易实现,程序只需将输入中的每个元素处理一次(甚至不需要缓存文档),但这种方法可能会令用户感到烦恼,他们必须等待很长时间,直到显示所有的文本。
另一种方法是串行执行。它先绘制文本元素,同时为图像预留出矩形的占位空间。在处理完第一遍文本后,程序开始下载图像,并将它们绘制到相应的占位空间中。
但是,图像下载过程大部分时间都是在等待IO操作执行完成,这期间CPU几乎不做任何操作,因此串行执行方法没有充分利用CPU,显然正确的做法是将问题分解为多个独立的任务并发执行,从而获得更高的CPU利用率和响应灵敏度。
// 串行地渲染页面元素
//图像下载过程大部分时间都是在等待IO操作执行完成,这期间CPU几乎不做任何操作,因此串行执行方法没有充分利用CPU,
public abstract class SingleThreadRenderer
{
void renderPage(CharSequence source)
{
//CharSequence是char类型的值的一个可读序列
renderText(source); //渲染文本
List<ImageData> imageData=new ArrayList<ImageData>();
for(ImageInfo imageInfo:scanForImageInfo(source))
imageData.add(imageInfo.downloadImage()); //下载图片
for(ImageData data:imageData)
renderImage(data); //渲染图片
}
}
2、携带结果的任务Callable与Future
Executor框架使用Runnable作为其基本的任务表示形式。Runnable是一种有很大局限的抽象,虽然run能写入到日志文件或者将结果放入某个共享的数据结构中,但它不能返回一个值或抛出一个受检查的异常。
许多任务实际上都是存在延迟的计算——执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。 对于这些任务,Callable是一种更好的抽象:它认为主入口点(即call)将返回一个值,并可以抛出一个异常。
Runnable和Callable描述的都是抽象的计算任务。这些任务通常是有范围的,即都有一个明确的起始点,并且最终会结束。Executor执行的任务有4个生命周期阶段:创建,提交,开始和完成。
由于有些任务可能要执行很长时间,因此通常希望能够取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们响应中断时,才能取消。取消一个已经完成的任务不会有任何影响。
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。
// Future接口
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方法介绍
- get方法的行为取决与任务的状态(尚未开始,正在运行,已完成)。如果任务已完成,那么get会立即返回或抛出一个Exception。 如果任务没有完成,那么get将阻塞并直到任务完成。如果任务抛出了异常,那么get将异常封装为ExecutionException并重新抛出。如果任务被取消,那么get将抛出CancellationException。如果get抛出了ExecutionExecutor,那么可以通过getCause来获得被封装的初始异常。
可以通过多种方法来创建一个future来描述任务。ExecutorService中的所有submit方法都将返回一个Future,从而将一个Runnable或Callable提交给Executor,并得到一个Future用来获得任务的执行结果或取消任务。
还可以显示地为某个指定的Runnable或Callable实例化一个FutureTask(由于FutureTask实现了Runnable,因此可以将它提交给Executor来执行,或者直接调用它的run方法)。
ExecutorService实现可以改写AbstractExecutorService中的newTaskFor方法,从而根据已提交的Runnable或Callable来控制Future的实例化过程。在默认实现中仅创建了一个新的FutureTask。
//newTask的默认实现
protected <T> RunnableFuture<T> newTaskFor(Callable<T> task)
{
return new FutureTask<T>(task);
}
3、使用Future实现页面渲染器
之前在第五章时就有提到,可以用Future Task来提前加载一些可能用到的资源。为了使页面渲染器实现更高的并发性,首先将渲染过程分解为两个任务,一个是渲染所有的文本,一个是下载所有的图像(因为其中一个任务是CPU密集型,而另一个任务是I/O密集型,因此这种方法即使在单CPU系统上也能提升性能)。
Callable和Future有助于表示这些协同任务之间的交互。例如,在FutureRenderer中创建一个Callable来下载所有的图像,并将其提交到一个ExecutorService。这将返回一个描述任务执行情况的Future。
当主任务需要图像时,它会等到Future.get调用结果,如果幸运的话,当开始请求时所有图像就已经下载完成,即使没有,至少图像的下载任务也已经提前开始了。先新建加载图片的Future task,再执行加载文本操作,等到主任务调用图片时,加载图片的任务可能已经完成,至少也先加载了。
// 使用Future预先加载图片
public class FutureRenderer
{
private final ExecutorService executor=Executors.newCachedThreadPool();//新建线程池
void renderPage(CharSequence source)
{
final List<ImageInfo> imageInfos=scanForImageInfo(source);//页面信息
// 新建了一个Callable来下载所有的图像。
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; //Callable会返回结果
}
};
//提交一个带返回值的任务,并且返回一个代表即将得到任务的结果的Future
Future<List<ImageData>> future=executor.submit(task);
renderText(source); //渲染文本
try{
//当主任务需要图像时,它会等到Future.get调用结果,如果幸运的话,当开始请求时所有图像就已经下载完成,即使没有,至少图像的下载任务也已经提前开始了。
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.launderThrowable(e.getCause());
}
}
}
get方法拥有“状态依赖”的内在特性,因而调用者不需要知道任务的状态,此外在任务提交和获得结果中包含的发布属性也确保了这个方法是线程安全的。FutureRenderer使得渲染文本任务与下载图片数据的任务并发地执行。当所有图像下载完成后,会显示到页面上,这将提升用户体验,不仅使用户更快地看到结果,还有效利用了并行性。
但我们还可以做得更好,用户不用等待所有的图像都下载完成,而希望看到每当下载完一幅图像就立刻显示出来。
4、在异构任务并行化存在的局限
异构任务指的是多者协同、共同完成的任务。
在FutureRenderer中,我们尝试并行地执行两个不同类型的任务-下载图像与渲染页面。然而,通过对异构任务进行并行化来获得重大的性能提升是困难的。
书中原话,两个人可以很好地分担洗碗的工作:其中一个人负责清洗,另一个负责烘干。然而,要将不同类型的任务平均分给每个工人并不容易。当人数增加时,如果确保他们能帮忙而不是妨碍其他人工作,或者在重新分配工作时,并不是容易的事。如果没有在相似的任务之间找出细粒度的并行性,那么这种方法的好处将减少。
当在多个工人之间分配异构的任务时,还有一个问题就是各个任务的大小可能完全不同。
当在多个工人之间分解任务时,还需要一定的任务协调开销:为了使任务分解能提高性能,这种开销不能高于并行性实现的提升。
FutureRenderer使用了两个任务,其中一个复杂渲染文本,另一个负责下载图像。如果渲染文本的速度远远高于下载图像速度(可能性很大),那么程序的最终性能与串行执行相比性能差别不大,而代码却复杂了。虽然做了很多工作来并发执行异构任务以提高并发度,但从中得到的并发性却是十分有限的。
只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。补充:同构:多个相同类型的物体参与完成某一件事情,异构:多个不同类型的物体参与完成某一件事情。
5、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中。
// 由ExecutorCompletionService使用的QueueingFuture类
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);
}
}
6、示例:使用CompletionService实现页面渲染器
可以通过CompletionService从两个方法来提高页面渲染器的性能:缩短总时间以及提高响应性。为每一幅图像的下载都创建一个独立任务,并从线程池中执行它们,从而将串行的下载过程转化为并行的过程,最终减少下载的总时间。
此外,通过CompletionService中获取结果以及使每张图片在下载完成后立刻显示出来,使用户获得一个更加动态和更高响应性的用户界面。
// 使用CompletionService,使页面在下载完成后立即显示出来
public abstract class Renderer {
//CompletionService将Executor与BlockingQueue的功能融合在一起
private final ExecutorService executor;
Renderer(ExecutorService executor){
this.executor=executor;
}
void renderPage(CharSequence source){//缓存页面
List<ImageInfo> info=scanForImageInfo(source);
//ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。
CompletionService<ImageData> completionService=
new ExecutorCompletionService<ImageData>(executor);
for(final ImageInfo imageInfo:info)
//提交一个带返回值的任务,并且返回一个代表即将得到任务的结果的Future
completionService.submit(new Callable<ImageData>(){
public ImageData call(){
return imageInfo.downloadImage(); //CompletionService中有类似队列的操作
}
});
renderText(source);
try{
//通过CompletionService中获取结果以及使每张图片在下载完成后立刻显示出来
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.currentThread().interrupt();
}catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
7、为任务设置时限
有时候,如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。
例如,某个Web应用程序从外部的广告服务器上获取广告信息,但如果该应用程序在两秒内得不到响应,那么将显示一个默认的广告,这样即使不能获得广告信息,也不会降低站点的响应性能。
在支持时间限制的Future.get中支持这种需求:当结果可用时,它立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。
在使用限时任务时应注意,当这些任务超时后应该立即停止,从而避免为继续计算一个不再使用的结果而浪费计算资源。要实现这个功能,可以由任务本身来管理它的限定时间,并且在超时后终止执行或取消任务。此时可再使用Future,如果一个限时的get方法跑出了TimeoutException,那么可以通过Future取消任务。
RenderWithTimeBudget 给出了限时Future.get的一种典型应用。在它生成的页面中包括响应用户请求的内容以及从广告服务器上获得广告。它将获取广告的任务提交给一个Executor,然后计算剩余的广告文本内容,最后等待广告信息,知道超出限定的时间(传递给get的timeout参数的计算方法是,将指定时限减除当前时间,这可能会得到负数,但在这里与时限有关的负数都视为0)。如果get超时,那么将取消广告获取任务,并转而使用默认的广告信息。
// 在指定时间内获取广告信息
public class RenderWithTimeBudget {
private static final Ad DEFAULT_AD = new Ad();
private static final long TIME_BUDGET = 1000;
private static final ExecutorService exec = Executors.newCachedThreadPool();
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.nanoTime();
ad = f.get(timeLeft, NANOSECONDS);//在指定时限内获取,NANOSECONDS为时间单位
}catch (ExecutionException e) {
ad=DEFAULT_AD;
}catch (TimeoutException e) {
//如果超时了,广告转为默认广告,并取消获取任务
ad=DEFAULT_AD;
f.cancel(true);
}
page.setAd(ad); //为页面设置广告
return page;
}
Page renderPageBody() { return new Page(); }
static class Ad {
}
static class Page {
public void setAd(Ad ad) { }
}
static class FetchAdTask implements Callable<Ad> {
public Ad call() {
return new Ad();
}
}
}
8、示例:旅行预定客户网站
考虑这样一个旅行预定门户网站:用户输入旅行的日期和其他要求,门户网站获取并显示来自多条航线,旅店或汽车租赁公司的报价。可能会调用Web服务,访问数据库,执行一个EDI事物或其他机制。
在这种情况下,不宜让页面的响应时间受限于最慢的响应时间,而应该只显示在指定时间内收到的信息。对于没有及时响应的服务提供者,页面可以忽略或者显示提示信息。
从一个公司获取报价的过程与其他公司获得报价的过程无关,因此可以将获取报价的过程当成一个任务,从而使获得报价的过程能并发执行。创建n个任务,将其提交到线程池,保留n个Futrue,并使用限时的get方法通过Future串行地获取每一个结果,这一切都很简单,但我们还可以使用一个更简单的方法——invokeAll(invoke 援引)
下面的代码使用了支持显示的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()throw 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));
//InvokeAll方法的参数为一组任务,并返回一组Future ,用时限来限制时间
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 {
//invokeAll按照任务集合中迭代器额顺序肩所有的Future添加到返回的集合中
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;
}
总结:Executor框架将任务提交和执行策略解耦开,同时还支持多种不同类型的执行策略,当需要常见线程执行任务时,可以考虑使用Executor,在分解任务时,要定义清晰的任务边界。