任务执行
大多数并发应用程序都是围绕‘任务执行’来构造的:通常是一个抽象且离散的工作单元,通过把应用程序的工作分解到多个任务中。
在线程中执行任务
当围绕‘任务执行’设计应用程序结构,第一步就是找出清晰的任务边界,理想情况下,各个任务是独立的:任务并不依赖其他任务的状态或结果。这些独立的任务都可以并发执行。
大多数服务器应用都提供了一种自然的任务边界:以独立的客户请求为边界。
串行的执行任务
在应用程序中可以通过多种策略来调度任务,最简单的策略就是在单个线程中串行的执行各项任务
SingleThreadWebServer很简单,且理论上是正确的,但在实际生产中性能却非常差。在web请求中可能会因为网络拥塞和处理IO或数据库请求等陷入阻塞,但单线程中阻塞不但会延迟当前请求的完成时间,还会彻底阻止等待中的请求被处理。用户将认为服务器是不可用的,因为服务器看似失去了响应。
显示的为任务创建线程
通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。
对于每个连接,主循环都创建一个新的线程处理请求。
- 任务处理过程从主线程分离出来。
- 任务可以并行的处理
- 任务处理代码必须是线程安全的,因为会有多个任务并发的调用这段代码
无限制创建线程的不足
- 线程生命周期的开销非常高,线程的创建和销毁并不是没有代价的,线程的创建会需要时间,会延迟处理的请求,如果请求频率非常高,且请求处理过程是轻量级的,那么为每个请求创建一个新线程会消耗大量的计算资源
- 资源消耗。活跃的线程会消耗系统资源,尤其是内存,如果可运行的线程多余可用处理器的数量,那么有些线程会被闲置,大量的空闲线程会占用许多内存,并且大量的线程在竞争CPU资源,还将产生其他的性能开销,如果有了足够多的线程,那么再创建反而会降低性能
- 稳定性。在可创建线程的数量上存在一个限制,破坏了这个限制可能抛出oom异常,过多的创建线程,那么整个应用也会崩溃。避免这个危险,应该对可创建的线程进行限制。
Executor
任务是一组逻辑工作单元,而线程是任务异步执行的机制。上面已经分析或串行执行和无限制分配新线程的方式,都存在一些严重的缺陷。java中提供了一种灵活的线程池作为Executor框架的一部分,在java类库中,任务执行的主要抽象不是Thread,而是Executor。
Executor框架能够支持多种不同类型执行策略,它将任务提交的过程与执行的过程解耦开,并用Runnable表示任务,Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。
通过使用Executor,将请求处理任务的提交与任务的实际执行解耦开,并且只需采用另一种不同Executor的实现,就可以使用不同的执行策略。
比如:
可以很容易的修改它的执行策略。
执行策略
通过将任务的提交与执行解耦开来,从而无须太大的困难就可以为某种类型的任务指定和 修改执行策略。在执行策略中定义了任务执行的“What、Where、When、How”等方面,包括:
- 在什么(What)线程中执行任务?
- 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)?
- 有多少个(How Many)任务能并发执行?
- 在队列中有多少个(How Many)任务在等待执行?
- 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝?
- 在执行一个任务之前或之后,应该进行哪些(What)动作?
线程池
线程池:管理一组工作线程的资源池。线程池与工作队列密切相关,工作队列保存着所以等待执行的任务,工作线程,从队列中获取一个任务,执行任务,返回线程池并等待下一个任务。
线程池的优势:
- 可以复用老的线程而不是创建新线程。
- 当请求到达时,线程已经存在,因此不会有等待线程创建而延迟任务的执行,提高了响应性。
- 可以限制线程池的大小,不会过多的创建线程
类库提供了一些默认的线程池
- newFixedThreadPool:newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。
- newCachedThreadPool:newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
- newSingleThreadExecutor:newSingleThreadExecutor是一个单线程的 Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。newSingleThreadExecutor能确保依照任务在队列中的顺序来串行执行(例如FIFO、LIFO、优先级)。
- newScheduledThreadPool:newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务。
Executor的生命周期
为了解决服务执行生命周期的问题,Executor扩展了ExecutorService接口,提供了一些管理生命周期的方法。
ExecutorService的生命周期有3种状态:运行、关闭和已终止。ExecutorService在初始创建时处于运行状态。
shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成-包括那些还未开始执行的任务。
shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
在ExecutorService关闭后提交的任务将由“拒绝执行处理器(Rejected Execution Handler)”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的 Rejected ExecutionException。等所有任务都完成后,ExecutorService 将转入终止状态。
可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询 ExecutorService是否已经终止。通常在调用 awaitTermination之后会立即调用shutdown,从而产生同步地关闭ExecutorService的效果。
延迟任务与周期任务
Timer类负责管理延迟任务(延迟几秒执行)和周期任务(隔几秒执行一次)。然而它存在着一些缺陷,应该使用ScheduledThreadPoolExecutor来替代它。 Timer存在的问题:
- Timer在执行所以定时任务都只会创建一个线程,如果其中某个任务执行时间过长,那么会破坏其他任务定时精确性。例如某个周期TimerTask需要每10ms执行一次,而另一个TimerTask需要执行40ms,那么这个周期任务或者在40ms任务执行完成后快速连续地调用4次,或者彻底“丢失”4次调用(取决于它是基于固定速率来调度还是基于固定延时来调度)。线程池能弥补这个缺陷,它可以提供多个线程来执行延时任务和周期任务。
- 如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不捕获异常,因此当TimerTask 抛出未检查的异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误地认为整个Timer都被取消了。因此,已经被调度但尚未执行的TimerTask将不会再执行,新的任务也不能被调度。(这个问题称之为“线程泄漏”)
示例:串行的页面渲染器
页面的渲染分为文字和图片,图片下载都是在登录IO操作执行完成,这段时间CPU不做任何操作,使得响应时间很长,可以将任务分解成并行执行,提高响应度。
携带结果的Future和CallAble
Executor使用Runnable作为一个基本任务,但是它没有返回值,或者抛出一个受检异常。
Runnable 和 Callable描述的都是抽象的计算任务。这些任务通常是有范围的,即都有一个明确的起始点,并且最终会结束。Executor执行的任务有4个生命周期阶段:创建、提交、开始和完成。由于有些任务可能要执行很长的时间,因此通常希望能够取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们能响应中断时,才能取消。取消一个已经完成的任务不会有任何影响。
Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在程序清单6-11中给出了Callable 和Future。在Future规范中包含的隐含意义是,任务的生命周期只能前进,不能后退,就像ExecutorService的生命周期一样。当某个任务完成后,它就永远停留在“完成”状态上。
get方法
- 任务完成:返回结果或者抛出一个Exception。
- 任务没有完成:get一直阻塞,直到任务完成。
- 任务抛出异常:将异常封装成ExecutionException并抛出,可以通过getCause来获取被封装的异常。 ExecutorService的submit方法会返回一个Future,用于获取任务的返回,或者取消任务。
Future<Object> future = Executors.newFixedThreadPool(1).submit(new Callable<Object>() {
@Override
public Object call() {
return null;
}
});
FutureTask
FutureTask实现了RunnableFuture,RunnableFuture又继承了Runnable, Future。
即可以通过Runnable接口实现线程,也可以通过Future取得线程执行完后的结果),因此FutureTask也可以直接提交给Executor执行。
Callable<String> call = new Callable<String>() {
@Override
public String call() throws Exception {
return "";
}
};
FutureTask<String> futureTask = new FutureTask<String>(call);
Executors.newFixedThreadPool(1).submit(futureTask);
futureTask.get();
使用Future实现页面渲染器
上图虽然将渲染分成了2个任务,渲染文字和下载图片。但一般来说,渲染文本的速度远远快于下载图片的速度。那么程序提升的性能不大(因为下载图片还是在串行下载),但是代码的复杂度却变高了。只有大量独立且同构的任务可以并发执行时,性能才能真正提升。很明显这里的下载图片是相互独立的。
CompletionService
如果向Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后反复使用get方法,同时将参数timeout指定为0,从而通过轮询来判断任务是否完成。这种方法虽然可行,但却有些繁琐。幸运的是,还有一种更好的方法:完成服务(CompletionService)。
CompletionService 将Executor和BlockingQueue的功能融合在一起。你可以将Callable 任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时将被封装为Future
CompletionService的实现非常简单:
提交某一个任务时,封装成一个QueueingFuture,QueueingFuture其实就是继承了FutureTask重写了done方法(任务完成时,回调该方法)。BlockingQueue<Future> completionQueue 用来存储返回结果。
为任务设置时限
某个任务如不能在一定时间内完成,那么将不再需要它的结果。如一个门户网站从多个数据源获取显示的数据,如果超出了指定时间,那么只显示已经获取的数据。Future.get支持这种需求,在指定时限内没有获取结果,抛出一个TimeoutExecption。如果一个Future抛出了TimeoutExecption,那么可以通过Future取消任务。
示例:旅行预订门户网站
一个旅行门户网站,用户输入日期和其他要求,门店获取并展示来自多条航线,多个旅馆的报价。在获取报价的过程中,如果指定时间内没有响应,那么就不予显示。
从一个公司获得报价的过程与从其他公司获得报价的过程无关,因此可以将获取报价的过程当成一个独立的任务,从而使获得报价的过程能并发执行。创建n个任务,将其提交到一个线程池,保留n个Future,并使用限时的get方法通过Future 串行地获取每一个结果,这一切都很简单,但还有一个更简单的方法-invokeAll。
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> getRankedTrave1Quotes(
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.invokeA11(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));
}
}
return quotes;
}
}
InvokeAll方法的参数为一组任务,并返回一组Future。这两个集合有着相同的结构。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能将各个 Future与其表示的Callable关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll将返回。当超过指定时限后,任何还未完成的任务都会取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,而客户端代码可以调用 get或 isCancelled 来判断究竟是何种情况。