在线程中执行任务
围绕着任务执行来设计程序结构时,第一步就是要找出清晰的任务边界。在理想状态下各个任务之间是相互独立的;任务并不依赖余其他任务的状态、结果或边界效应。独立性有助于实现并发因为如果存在足够多的处理资源,那么这些任务都可以并行执行(个人理解:每个任务相互独立,没有影响。在处理资源足够多的情况下,能做到真正的并行执行。如果没有独立,可能某个线程执行到某一地方后再向下执行要等待其他线程的完成资源,那么要等待其他线程处理完成。就不是并行执行)。要做到良好的吞吐量和快速的响应性,应该选择清晰的任务边界以及明确的任务执行策略。
大多数服务器是由应用程序提供了一种自然的任务边界选择方式:以独立的客户请求为边界(个人理解:将每个客户线程都独立开来,不互相影响)
串行的执行任务
单个线程中串行的执行各项任务。代码如下
我们可以看到串行的处理程序很简单,但是呢如果有多个IO操作的请求过来了。那么handleRequest要执行很久的时间,那么多个请求将阻塞再ss.accept()那里。这样cpu的利用率是极低的,因为单线程在等待I/O操作完成时,CPU将处于闲置状态。
显示地为任务创建线程
对待每一个请求新建一个线程,代码如下
任务处理过程和从总任务中分离出来,使得主循环能够更快的等待下一个到来的连接,从而提高响应性。任务可以并行处理,从而能同时服务多个请求。对于这种方式,只要请求的到达速率不超出服务器的处理能力,那么这种方法可以同时带来更快的响应性和更高的吞吐量。
无限制创建线程的不足
在实际生产环境中,为每个人物创建一个线程这样的做法是存在下面几种缺陷的。
- 线程生命周期的开销非常高。如果对于请求多且大量的请求都是轻量级的请求是,那么所有线程的创建和销毁的开销是非常大的。
- 资源消耗。线程过多对于内存会造成很大的压力,而且大量的线程在竞争cpu资源的时候还将产生其他的性能开销。
- 稳定性。在一定的范围内,增加线程可以提高系统的吞吐率。但超出了这个范围增加线程可能会造成系统的崩溃。
Executor框架
Executor它提供了一种标准的方法将人物的提交过程与执行过程解耦开来,并用Runnable来表示任务。(个人理解:此种解耦应该是使用该框架的过程中,某个线程将任务进行提交,而另一个线程负责人物的执行)。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。
基于Executor的web服务器
使用线程池来避免线程的大量创建与销毁,并且给创建线程的数量做一个限制。
通常Executor是一次性配置的,因此在部署阶段可以完成,而提交任务的代码会不断的扩散到整个程序中,增加了修改的难度。
执行策略
执行策略中定义了任务执行的各方面,包括:
- 在什么(what)线程中执行任务。
- 任务按照什么顺序执行(FIFO,LIFO,优先级)
- 有多少个(How Many)任务能并发执行
- 在队列中有多少个(How Many)任务在等待执行
- 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝?
- 在执行一个任务之前或之后,应该进行哪些(What)动作。
线程池
- newFixedThreadPool。创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期Exception而结束,那么线程池会补充一个新的线程)
- newCachedThreadPool。创建一个可缓存的线程池。如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何线程。
- newSingleThreadExecutor。是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。该池能去报依照任务在队列中的顺序来串行执行。
- newScheduledThreadPool。创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务。
通过使用Executor,可以实现各种调优,管理,监视,记录日志,错误报告和其他功能,如果不使用任务执行框架,那么要增加各种功能非常困难。
Executor的生命周期
现在讨论Executor怎么去关闭。因为jvm只有在所有(非守护)的线程全部终止后才会退出。所以Executor没有正确关闭那么jvm将无法结束。
由于Executor是异步执行任务,所以在关闭时,可能有任务还在运行,有些还在等待。所以在关闭时可能采用比较平缓的关闭形式(完成所有已启动的任务并且不在添加新的任务),也可能采用最粗暴的关闭形式(关闭机房的电源),Executor是可关闭的,并将关闭操作中受影响的任务的状态反馈给应用程序。
ExecutorService该接口封装了Executor的生命周期管理方式
延迟任务与周期任务
Timer类负责管理延迟任务以及周期任务。Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来替代它。
Timer在执行任务时只会创建一个线程,那么将破坏精确性。比如一个Task10m执行一次,但另一个task需要执行40m。那么可能在执行完后快速的连续调用4次,或者4次全部丢失。另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer并不捕获异常,因此当TimerTask抛出未检查异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误的认为整个Timer被取消了。这称之为线程泄露。
错误的Timer使用方法
找出可利用的并行性
Executor框架帮助指定执行策略,但如果要使用Executor,必须将任务表述为一个Runnable。在大多数服务器应用程序都存在一个明显的任务边界:单个客户请求,但有时候任务边界并非显而易见。例如在很多桌面应用程序中,即使是服务器应用程序,在单个客户请求中仍可能存在可发掘的并行性。例如数据库系统。();
串行的渲染器
当下载文档和图片时,可能先获取图片的引用,然后再绘制到图片缓存中。最后绘制文本元素。这可能会让用户难过,要等待好久才会显示。另一种串行方式更好一些,那就是先绘制文本,同时为图像腾出预留空间。处理完一片文本后,在开始下载图像,并将它们绘制到相应的占位空间中。
代码如下所示。
携带结果的任务Callable与Future
Runnable是一种有很大局限性的抽象。因为他不能抛出异常和带返回值
在对于某些任务,Callable是一个更好的选择,他将返回一个值,并且可能抛出一个异常。要使用Callable表示一个没有返回值的任务,可以使用Callable<Void>.。Executor执行的任务有4个生命周期阶段:创建,提交,开始,完成。Future表示一个任务的生命周期,比提供了相应的方法来判断是否完成或取消,以及获取任务的结果和取消任务等。
使用Future实现页面渲染器
为了使页面渲染器实现更高的并发性,首先将渲染过程分为两个任务,一个是渲染所有的文本,另一个是下载所有的图像。(因为其中一个任务是CPU密集型,一个是IO密集型。所以该方法在单核系统上也能提高性能(个人理解:因为当一个线程执行IO操作后,线程将释放资源处于空闲状态,从而另一个任务的cpu资源充足))。
使用这种方式,这将提升用户的体验,不仅使用户更快的看到结果,还有效的利用率并行。但我们可以做的更好,因为不需要用户等待所有图片下载完,而是应该每一张图片下载完就显示给用户观看
在异构任务并行化中存在的局限
在上面的示例中,我们尝试并行的执行两个不同类型的任务—下载图像与渲染页面。然而,通过对异构任务进行并行化来获得重大的性能提升是很困难的。(个人理解:因为并行化进行页面加载,加载完所有页面的总时间是按照最慢的来。可能加载图片的时间要远远大于加载文本的时间,那么性能将没有多大的提升)。当使用两个线程时,至多能将速度提高一倍。因此虽然做了许多工作来并发执行异构任务以提高并发度,但从中获得的并发性确是十分有限的。
只有当大量相互独立且通过的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能的提升。
使用CompletionService实现页面渲染器
通过CompletionService从两个方面来提高页面渲染器的性能:缩短总运行时间以及提高响应性。为每一幅图像的下载都创建一个独立任务,并在线程池中执行他们,从而将串行的下载过程转换为并行的过程:这将减少下载所有图片的总时间。
为任务设置时限
有些时候在线程跑了一段时间我就不想让其在运行,而是给出默认的一个内容。这样可以确保系统资源的低消耗。
、
旅行预订门户网站
在有些情况下,不宜让页面的响应的服务提供者,页面可以忽略它们,或者显示一个提示信息。
InvokeAll,将多个任务提交到一个ExecutorService并获得结果。InvokeAll方法的参数为一组任务,并返回一组Future。当超过指定时限后,任何还未完成的任务都会取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,而客户端代码可以调用get或isCancelled来判断究竟是何种情况。