java并发学习(4)

来源:互联网 发布:linux vi向上翻页 编辑:程序博客网 时间:2024/05/13 22:33

串行的web服务器显然是效率低下的,它每次只接收一个任务,并且要等任务结束后才能接受下一个人物,无论是吞吐量还是响应性都十分的差。


主线程负责接收连接,主线程创建出来的新线程负责处理请求。

优点

1:任务处理过程从主线程中分离出来,提高响应性

2:任务并行处理,程序的吞吐量提高

3:任务处理过程必须是线程安全的,因为任务处理代码是所有线程共享。

由无限创建线程引起的缺点

1:线程生命周期的开销非常大,如果请求非常频繁,而任务处理过程非常简便,这使得创建线程的开销非常大

2:内存损耗,主要是无限创建线程会引起大量的闲置线程,这部分线程会浪费内存资源

3:在可创建的线程的数量上存在一个限制,这个限制随平台不同而不同,总之无限创建线程会引起outofmemoryError


Executor框架为异步任务执行提供了多重不同的执行策略(注释1),它通过一种标准的方法将任务的提交与执行解耦开来,并用runnable来表示任务,Executor基于生产者消费者模式,提交任务相当于生产者,执行任务相当于消费者,(其中有界队列有效防止了耗尽内存)


从为每一个任务分配线程策略变成了基于线程池的策略,web服务器将不会在高负载情况下失败,因为不会出现过多的线程争夺有限的cpu和内存而导致服务器崩溃(其中依然有内存耗尽的可能性。等待补充)


由于使用到了executor框架,所以我们必须了解其生命周期并正确关闭它,因为jvm只有当所有非守护线程退出时才回关闭。executor拓展了executorService接口,添加了管理生命周期的方法(shutdown,shutdownnow(注释7),isterminated,awaittermination)

executorService的生命周期有三种状态,运行,关闭,已终止。executorService在初始创建时为运行状态,关闭方法有shutdown(执行完已经在队列中的任务并不再接收新的任务)和shutdownnow(取消所有运行中的任务并且不再接收新任务)此时生命周期达到关闭状态,而继续提交的任务将由rejectedExecutionhandler来处理(注释4),等待所有任务处理完毕,生命周期达到终止状态,awaittermination用来等待executorService到达终止状态,isterminated来轮询其是否终止。


提高程序并行性的示例

示例1:串行地渲染页面元素


先处理source,绘制文本元素,并为图片留下占位空间,然后再从source中加载出图片,将图片载入占位空间。显然这种串行方法大多数时间都在等待IO操作完成

示例2:使用future实现的页面渲染器


executor创建了一个callable(注释5)线程(其带有下载图片的结果)并返回一个future(注释5)。主线程渲染文本并且通过future.get获得callable线程下载的图片。其中两条线程并行执行,比起示例一响应速度更快

示例3:使用completionService实现页面渲染器



在异构任务(不同类型的任务)并行化中存在的局限是非常大的。例如,不同任务A和B,完成A需要10分钟,完成B需要90分钟。由一个人来完成的话,最终需要100分钟,如果又两个人来完成的话,则最终需要90分钟,而效率只提高了0.9倍,即当两个不同任务耗时相差很大的时候,并行化并不能有效提高性能,不仅如此,由于并行化需要消耗资源(如创建线程等),有可能导致并行化的效果还不如串行。(关于线程优化,另起篇章)。故能得到结论。只有当大量相互独立且同构的任务并发处理时,才能体现出并发的优势

所以示例3较之于示例2只是多了一个在下载图片时候的并行处理,其中使用了completion(注释6)批处理来处理图片数组(同构任务)

注释1:执行策略定义了任务执行的“what,what,how many,how many,which,how,what”,

*在什么线程中执行任务

*以什么样的顺序执行任务 FIFO/FILO

*有多少任务可以并发执行

*多少任务等待执行

*当线程过载时候,应拒绝哪个任务

*如何通知应用程序某个任务被拒绝

*在执行任务前后,需要进行什么动作

注释2:线程池,管理一组同构工作线程的资源池,线程池与工作对联紧密相联,工作线程的任务就是从工作队列中获取一个任务,执行任务完毕后返回线程池。

在线程池中执行任务(有限线程)比为每个任务创建一个线程(无限线程)优势多的多

1:重复使用现有线程,减少了线程创建和销毁的开销

2:线程的创建需要时间,重用线程减少了这部分时间,提高响应性

Executors中的静态工厂提供了六种线程池:

newFixedThreadPool:线程池长度固定,每提交一次任务,创建一个线程池,当线程池最大时,规模不发生变化,当某个线程由于异常而结束时,线程池会创建一个新线程

newCacheThreadPool,当线程池当前规模超过需求时,会回收空闲线程,当线程池规模不足时,会创建新线程,该线程池规模不受任何限制

newSingleThreadPool,创建单线程来执行任务,当该线程异常结束时会创建新的线程来代替。这个线程池可以保证队列中的任务串行执行。

newScheduledThreadPool,创建一个固定长度的线程池,而且以延迟或者定时的方式执行任务,类似于timer(注释3)


注释3:timer类,负责管理延迟任务(在X秒后执行)和周期任务(每X秒执行一次),然而timer(基于绝对时间)由于存在一些缺陷,我们应使用ScheduledThreadExecutor(基于相对时间)来代替他

缺陷1:timer在执行定时任务时为单线程,这意味着,当执行某个任务的时间大于周期任务所需时间时,会发生快速连续调用或者任务丢失,例如,一个周期任务需要每一秒执行一次,而其中有个任务执行了三秒。则这个周期任务会快速连续执行三次或者丢失这三次任务。

缺陷2:当timer抛出一个未检查异常时,timer并不会捕获异常,因此线程将会被终止,而且timer也不会恢复线程的执行,而是错误的认为整个timer都被取消了,这种现象叫做线程泄漏

线程泄漏的主要原因是runtimeException(也叫uncheckException),出现这种异常一般意味着程序出错或者是不可修复的错误,因此他们不会被捕获,他们会在控制台输出栈追踪信息,并终止。这对单线程而言十分正常,但在并发情况下。一个线程发生线程泄漏时,可能整个应用程序还能正常运行,但是由于没人发现它而留下了安全隐患(例如GUI程序中丢失了事件分派线程,那么应用程序将停止处理时间并失去响应)

解决方法:1 try-catch处理可能抛出未检查异常的代码,2:使用uncaughtexceptionHandler(http://sunnylocus.iteye.com/blog/538282)

注释4:饱和策略用来处理当有界队列被填满时,或者一个任务被提交到已经关闭的executor中的情况

threadpoolexecutor的饱和策略可以通过setrejectedExecutionhandler来修改,其中包括四种饱和策略:abortPolicy,callerrunsPolicy,discardPolicy,discardOldestPolicy

Policy策略是默认的饱和策略,他会抛出未检查的rejectedExceutionException,调用者会捕获这个异常,然后根据需求编写自己的处理代码。当新提交的任务无法保存到队列中时,discardPolicy会将该任务抛弃,discardOldestPolicy,当队列满时,新的任务无法进入队列,策略会抛弃最旧的一个任务,然后重新提交新的任务。注意,当队列为优先队列时候,discardoldestpolicy会抛弃优先级最高的任务

callerRunsPolicy,该策略不抛出异常,也不抛弃任务,而是将任务返回到调用者身上(调用了execute方法的线程上),由于主线程需要执行被返回来的任务,故不会马上再提交新的任务到队列(即调用execute(task)),从而工作者线程有时间来处理任务。(此处应有冠以webserver的例子)

注释5:

executor使用runnable作为其基本的任务表现形势,但runnable是一个有很大局限性的抽象,尽管run能够写入日志或者将结果放入某个数据结构,但是他自身不能返回一个值或者受检查的异常,然而callable弥补了这个缺点

Future代表了一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取他任务的结果和取消方法,如同ExecutorService一样,其生命周期只能前进不能后退,并且一旦完成,就永远停留在完成状态上。

get方法的行为取决于任务的状态:1:已完成,get立即返回或者抛出一个异常,

2:未完成,get阻塞直到任务完成

3:任务抛出异常:get将异常封装为ExecutorException并重新抛出,我们可以通过getCause来获得被封装的初始异常

4:任务被取消:抛出一个CancellationException

ExecutorService中的所有submit方法都将返回一个Future,从而将一个callable或者runnable提交给executor并得到一个future来get任务的执行结果或者取消任务。

注释6:completionService将Executor和BlockingQueue的功能结合在一起,你可以将callable任务sunbmit给他来执行,然后通过take(阻塞方法)和poll(无阻塞方法)方法来获取已经完成的结果,而这些结果在完成时会被封装成executor。 executorCompletionService实现了completionService,并将计算部分委托给executor。

注释7:shutdownnow关闭executorservice的时候,他会取消所有正在运行的任务,拒绝尚未提交的任务,返回已经提交但尚未执行的任务,并将返回的任务写入日志或者保存起来以便日后处理。但是,我们无法知道哪些任务是正在执行中被取消的,所以。我们要想知道未完成的任务,我们不但需要shutdownnow返回的结果,还需要记录下被取消的任务。


0 0