Java并发编程实战:任务执行

来源:互联网 发布:java win 启动main 编辑:程序博客网 时间:2024/05/11 14:42

一、无限制创建线程的缺点:

(1)线程生命周期的开销:线程的创建与关闭是需要时间的,带来处理请求的延迟,并且需要在JVM和操作系统直接进行相应的处理活动。如果请求时频繁且轻量的,那么为每个请求创建一个新线程的做法会消耗大量的计算资源。

(2)资源消耗量:活动线程会消耗系统资源,尤其是内存。如果可运行的线程数多于可用的处理器数,线程将会空闲。大量空闲线程占用更多内存,给垃圾回收器带来压力,而且大量线程在竞争CPU资源时,还回产生其他的性能开销。

(3)稳定性:线程的数目会受到JVM的启动参数、Thread的构造参数中请求的栈大小等因素的影响,以及底层操作系统线程的限制,如果打破这些限制,最可能的结果就是会出现OutOfMemoryError。

二、Executor框架

Executor只是个简单的接口,但她却为一个灵活而强大的框架创造了基础,这个框架可以用于异步任务执行,而且支持很多不同类型的任务执行策略。它还为任务提交和任务执行之间的解耦提供了标准的方法,为使用Runnable描述任务提供了通用的方式。将任务的提交与任务的执行体进行解耦,它的价值在于让你可以简单地为一个类给定的任务制定执行策略,并保证后续的修改不会太困难。如下代码可以展示这一点:

class TaskExecutionWebServer{private static final ThreadPerTaskExecutor executor = new ThreadPerTaskExecutor();public static void main(String[] args) throws IOException {// TODO Auto-generated method stubServerSocket socket = new ServerSocket(80);while(true){final Socket connection = socket.accept();Runnable task = new Runnable(){public void run(){handleRequest(connection);}};executor.execute(task);}}}class ThreadPerTaskExecutor implements Executor{@Overridepublic void execute(Runnable r) {// TODO Auto-generated method stubnew Thread(r).start();//为每个任务创建线程,异步执行}}class WithinThreadExecutor implements Executor{@Overridepublic void execute(Runnable r) {// TODO Auto-generated method stubr.run();//将任务单线程化,所有的任务都同步地执行,在前一个任务从executor返回后再执行下一个};}

1、线程池

线程池管理一个工作者线程的同构池。线程池是与工作队列紧密绑定的。所谓工作队列,其作用是持有所有等待执行的任务。工作者线程从工作队列中获取下一个任务,执行它,然后回来继续等待另一个线程。

在线程池中执行任务的优势:

(1)重用存在的线程,而不是创建新的线程,这可以在处理多请求时抵消线程创建、消亡产生的开销。

(2)在请求到达时,工作者线程通常已经存在,不会因为等待创建线程而延迟任务的执行。

(3)通过适当地调整线程池的大小,可以得到足够多的线程以保持处理器忙碌,同时防止过多的线程相互竞争资源,导致应用程序耗尽内存。

通过调用Executors中的静态工程方法来创建一个线程池:

(1)newFixedThreadPool:创建一个定长的线程池,每当提交一个任务就创建一个线程,直到达到池的最大长度,这时线程池会保持长度不在变化(如果一个线程由于非预期的Exception而结束,线程池会补充一个新的线程)。

(2)newCachedThreadPool:创建一个可缓存的线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活地回收空闲的线程,当需求增加是,它可以灵活地添加新的线程,而不会对池的长度作任何限制。

(3)newSingleThreadExecutor:创建一个单线程化的executor,它只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它。

(4)newScheduledThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。

2、Executor的生命周期

为了解决执行服务的生命周期问题,ExecutorService接口扩展了Executor,并且添加了一些用于生命周期管理的方法(同时还有一些用于任务提交的便利方法)。如下代码是ExecutorService中用于生命周期管理的方法:

public interface ExecutorService extends Executor{void shutdown();List<Runnable> shutdownNow();boolean isShutdown();boolean isTerminated();boolean awaitTermination(long timeout,TimeUnit unit) throws InterruptedException;}
ExecutorService最初创建后的初始状态是运行状态。shutdown方法会启动一个平缓的关闭过程:停止接收新的任务,同时等待已经提交的任务完成——包括尚未开始执行的任务。shutdownNow方法会启动一个强制的关闭过程:尝试取消所有运行中的任务和排在队列中尚未开始的任务。

在关闭后提交到ExecutorService中的任务,会被拒绝执行处理器处理。拒绝执行处理器shicutorService的一种实现,ThreadPoolExecutor提供的,它可能只是简单放弃任务,也可能会引起execute抛出一个未检查的RejectedExecutionException。一旦所有的任务全部完成后,ExecutorService会转入终止状态。

3、延迟的、并具周期性的任务

Timer的缺点:                                                                                                                                                                                                                                         (1)Timer对调度的支持是基于绝对时间,而不是相对时间的,由此任务对系统时钟的改变是敏感的,而ScheduledThreadPoolExecutor只支持相对时间。因此应该考虑使用ScheduledThreadPoolExecutor作为代替品。

(2)Timer只创建唯一的线程来执行所有timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。调度线程池解决了这个缺陷,它让你可以提供多个线程来执行延迟、并具周期性的任务。

(3)Timer线程并不捕获异常,所以TimerTask抛出的未检查的异常会终止timer线程。这种情况下,Timer也不会再重新恢复线程的执行了;它错误地认为整个Timer都被取消了。此时,已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了,这个问题称为“线程泄露”。ScheduledThreadPoolExecutor可以妥善地处理这个问题。

4、可携带结果的任务:Callable和Future

Callable在主进入点call等待返回值,并为可能抛出的异常预先做好了准备。Executors包含了一下工具方法,可以把其他类型的任务封装成一个Callable。

Future描述了任务的生命周期,并提供了相关的方法来获得任务的结果、取消任务以及检验任务是否已经完成还是被取消。

以下是Callable和Future中提供的方法

public interface Callable<V>{V call()throws Exception;}public interface Future<V>{boolean cancel(boolean isCancel);boolean isCancelled();boolean isDone();V get() throws InterruptedException,ExecutionException,CancellationException;V get()(long timeout,TimeUnit unit)throws InterruptedException,CancellationException,TimeoutException;}
任务的状态决定了get方法的行为。

任务已完成:get会立即返回或者抛出一个Exception

任务未完成:get会阻塞直到它完成

任务被取消:get会抛出CancellationException

任务抛出异常:get会将该异常封装为ExecutionException,然后重新抛出;这时,可以用getCause重新获得被封装的原始异常。

创建一个描述任务的Future的方法:

(1)ExecutorService中所有的submit方法都返回一个Future,因此可以将一个Runnable或一个Callable提交给executor,然后得到一个Future。如下是示例代码:

public class FutureRenderer{private final ExecutorService executor ;void renderPage(CharSequence source){final List<ImageInfo> imageInfos = scanForImageInfo(source);Callable<List<ImageInfo>> task = new Callable<List<ImageData>>(){@Overridepublic List<ImageData> call() throws Exception {// TODO Auto-generated method stubList<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());}} }
(2)显式地为给定的Runnable或Callable实例化一个FutureTask,FutureTask实现了Runnable,所以既可以将它提交给Executor来执行也可以将它放在线程中执行。实例代码参见博客Java并发编程实战:并发基础构建模块。

5、CompletionService:当Executor遇见BlockingQueue

CompletionService整合了Executor和BlockingQueue的功能。ExecutorCompletionService是实现CompletionService接口的一个类,它在构造函数中创建一个BlockingQueue,用它来保存任务执行后返回的结果。当任务执行完成后会调用FutureTask中的done方法,QueueingFuture是FutureTask的一个子类,它覆写了父类的done方法,并在done方法中将结果添加到BlockingQueue中。当一个任务通过ExecutorCompletionService提交之后,首先会把这个任务包装为一个QueueingFuture,这样它的执行结果也就会被保存在ExecutorCompletionService的BlockingQueue中,我们就可以直接从阻塞队列中take和poll元素了。如下代码是CompletionService用法:

public class Renderer{private final ExecutorService executor ;void renderPage(CharSequence source){final List<ImageInfo> imageInfos = scanForImageInfo(source);CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor);for(final ImageInfo imageInfo : imageInfos){completionService.submit(new Callable<ImageData>(){@Overridepublic ImageData call() throws Exception {// TODO Auto-generated method stubreturn imageInfo.downloadImage();}});}renderText(source);try{for(int t = 0,n = imageInfos.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(e.getCause());}} }

6、为任务设置时限

限时版本的invokeAll将多个任务提交到一个ExecutorService,并获得其结果。InvokeAll方法处理一个任务的容器,并返回一个Future的容器。两个容器具有相同的结构;invokeAll将Future添加到返回的容器中,这样可以使用任务容器的迭代器,从而调用者可以将它的Callable与Future关联起来。当所有任务都完成时、调用线程被中断或者超过时限时,限时版的invokeAll都会返回结果。超过时限后,任何尚未完成的任务都会被取消。作为invokeAll的返回值,每个任务要么正常完成,要么被取消;客户端代码可以调用get或者isCancelled来查明是属于哪一种情况。








0 0
原创粉丝点击