[Java Concurrency in Practice]第六章 任务执行

来源:互联网 发布:淘宝9.9元天天特价包邮 编辑:程序博客网 时间:2024/05/18 14:22

任务执行

任务就是抽象、离散的工作单元。把一个应用程序的工作分离到任务中,执行与任务的分离,可以简化程序的管理。

服务器应用程序应该兼具良好的吞吐量和快速的响应性。

6.1 在线程中执行任务

当围绕“任务执行”来设计应用程序结构时,第一步就是要找出清晰地任务边界。在理想的情况下,各个任务之间是相互独立:任务并不依赖于其他任务的状态、结构或边界效应。独立有利于实现并发,因为如果存在足够多的处理资源,那么这些独立的任务都可以并行执行。为了在调度与负载均衡等过程中实现更高的灵活性,每项任务还应该表示应用程序的一小部分处理能力。

在正常的负载下,服务器应用程序应该同时表现出良好的吞吐量和快速的响应性。应用程序提供商希望程序支持尽可能多的用户,从而降低每个每个用户的服务成本,而用户则希望获得尽快的响应。而且,当负荷过载时,应用程序的性能应该是逐渐降低,而不是直接失败。要实现上述目标,应该选择清晰的任务边界以及明确的任务执行策略。

大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。Web服务器、邮件服务器、文件服务器、EJB容器以及数据库服务器等,这些服务器都能通过网络接受远程客户的连接请求。将独立的请求作为任务边界,既可以实现任务的独立性,又可以实现合理的任务规模。

6.1.1 串行地执行任务

在应用程序中可以通过多种策略来调度任务,而且其中一些策略能够更好地利用潜在的并发性。最简单的策略就是在单个线程中串行地执行各项任务。下面SingleThreadWebServer顺序地处理它的任务——接受达到80端吕的HTTP请求:

class SingleThreadWebServer {    public static void main(String[] args) throws IOException {        ServerSocket socket = new ServerSocket(80);        while (true) {            Socket connection = socket.accept();            handleRequest(connection);        }    }}

理论是正确的,但在实际环境中的执行性能却很糟糕,因为它每次只能处理一个请求。主线程在接受连接与处理相关请求等操作之间不断地交替运行。当服务器正在处理请求时,新到来的连接必须等待直到请求处理完成,然后服务器将再次调用accept。如果请求处理速度很快并且handleRequest可以立即返回,那么这种方法是可行的,但现实世界的Web服务器的情况却并非如此。

在Web请求的处理中包含了一组不同的运算与I/O操作。服务器必须处理套接字请求和写会响应,这些操作通常会由于网络阻塞或连通性问题而被阻塞。此外,服务器还可能处理文件I/O或者数据库请求,这些操作同样会阻塞。在单线程的服务器中,阻塞不仅会推迟当前请求的完成时间,而且还将彻底阻止等待中的请求被处理。如果请求阻塞时间过长,用户将认为服务器是不可用的,因为服务器看似失去了响应。同时,服务器的资源利用率非常低,因为当单线程在等待I/O操作完成时,CPU将处于空闲状态。

在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。也有一些例外,例如,当任务数量很少且执行时间很长时,或者当服务器只为单个用户提供服务,并且该客户每次只发出一个请求时——但大多数服务器应用程序并不是按照这种方式来工作的。在某些情况下,串行处理方式能带来简单性和安全性。大多数GUI框架都通过单一的线程来串行地处理任务。

6.1.2 显式地为任务创建线程

通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。如下程序所示:

class ThreadPerTaskWebServer {    public static void main(String[] args) throws IOException {        ServerSocket socket = new ServerSocket(80);        while (true) {            final  Socket connection = socket.accept();            Runnable task = new Runnable() {//使用线程处理每个请求                    public void run() {                        handleRequest(connection);                    }                };            new Thread(task).start();        }    }}

结构上类似于前面的单线程版本——主线程仍然不断地交替执行“接受外部连接”与“分发请求”等操作。区别在于,对于每个连接,主循环都将创建一个新线程来处理请求,而不是在主循环中进行处理。由此可得出3个主要结论:

  • 任务处理程序从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的请求。这使得程序在完成前面的请求之前可以接受新的请求,从而提高响应性。
  • 任务可以并行处理,从而能同时服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,例如等待I/O完成,获取锁或者资源可用性等,程序的吞吐量将得到提高。
  • 任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。

在正常的负载情况下,“为每个任务分配一个线程”的方法能提升串行执行的性能。只要请求的到达率不超出服务器的请求处理能力,那么这种方法可以同时带来更快地响应性和更高的吞吐率。

6.1.3 无限制创建线程的不足

在生产环境中,“为每个任务分配一个线程”这种方法存在一些缺陷,尤其是当需要创建大量的线程时:

  • 线程声明周期的开销非常高。线程的创建与销毁并不是没有代价的。根据平台的不同,实际的开销也有所不同,但线程的创建过程都会需要时间,延迟处理的请求,并且需要JVM和操作系统提供一些辅助操作。如果请求的到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为每个请求创建一个新线程将消耗大量的计算资源。
  • 资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程将闲置。大量空闲的线程会占用许多内存,给垃圾回收期带来压力,而且大量线程在竞争CPU资源时还将产生其他的性能开销。如果你已经拥有足够多的线程使CPU保持忙碌状态,那么再创建更多地线程反而会降低性能。
  • 稳定性。在可创建线程的数量上存在一个限制。这个限制值将随着平台的不同而不同,并且受多个因素制约,包括JVM的启动参数、Thread构造函数中请求的栈大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么很可能抛出OutOfMemoryError异常,要想从这种错误中恢复过来是非常危险的,更简单的办法是通过构造程序来避免超出这些限制。

在一定的范围范围内,增加线程可以提高系统的吞吐率,但如果超出了这个范围,再创造更多的线程只会降低程序的执行速度,并且如果过多地创建一个线程,那么整个应用程序将崩溃。要想避免这种危险,就应该对应用程序创建的线程数量进行限制,并且全面地测试应用程序,从而确保在线程数量达到限制时,程序也不会耗尽资源。

“每任务一线程”的问题在于它没有对创建的线程的数量进行任何限制,只限制了远程用户提交HTTP请求的速率。无限制的创建线程的行为在开发阶段可能表现还良好,但一旦部署后,并运行于高负载环境下,它的问题才会暴露出来。

6.2 Executor框架(任务与执行分开)

任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。前面两种通过线程来执行任务的策略,即把所有任务放在单个线程中执行,以及将每个任务放在各自的线程中执行。这两种方式都存在一些严格的限制:串行执行的问题在于其糟糕的响应性和吞吐量,而“为每个任务分配一个线程”的问题在于资源管理的复杂性。

就像“有界队列”防止应用程序过载而耗尽内存,线程池为线程管理提供了同样的好处。线程池简化了线程的管理工作,并且java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分。在Java类库中,任务执行的主要抽象不是Thread,而是Executor,如Executor接口所示:

public interface Executor {    void execute(Runnable command);}

Executor接口为灵活且强大的异步任务执行框架提供了基础,该框架能支持多重不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。

Executor基于生产者—消费者模式,提交任务的操作相当于生产者(生成待完成的工作单元),执行任务的线程则相当于消费者(执行完成这些工作单元)。如果要在程序中实现一个生产者—消费者的设计,那么最简单的方式通常就是使用Executor。

6.2.1 示例:基于Executor的Web服务器

class TaskExecutionWebServer {    private static final int NTHREADS = 100;    private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);    public static void main(String[] args) throws IOException {        ServerSocket socket = new ServerSocket(80);        while (true) {            final Socket connection = socket.accept();            Runnable task = new Runnable() {                public void run() {                    handleRequest(connection);                }            };            exec.execute(task);        }    }}

通过使用Executor,将请求处理任务的提交与任务的实际执行解耦开来,并且只需采用另一种不同的Executor实现,就可以改变服务器的行为。改变Executor实现或配置所带来的影响远远小于改变任务提交方式带来的影响。通常,Executor的配置是一次性的,因此在部署阶段可以完成,而提交任务的代码却会不断地扩散到整个程序中,增加了修改的难度。

只要作些简单的修改,就可以让TaskExecutionWebServer像ThreadPerTaskWebServer那样一样运行:替换一个Executor,它为每个请求都创建一个新的线程。我们只需这样实现:

public class ThreadPerTaskExecutor implements Executor {    public void execute(Runnable r) {        new Thread(r).start();    };}

或者如果要像SingleThreadWebServer那样运行,则可以让Executor在调用线程中同步地执行所有任务:

public class WithinThreadExecutor implements Executor {    public void execute(Runnable r) {        r.run();    };}

所以使用Executor是很方便的,它可以让你很方便的更改线程的执行机制,做到了任务与执行的解耦与真正分离。

6.2.2 执行策略

通过将任务的提交与执行解耦开来,从而无须太大的困难就可以为某种类型的任务指定和修改执行策略。在执行策略中定义了任务执行的“What、Where、When、How“等方面,包括:

  • 在什么(What)线程中执行任务?
  • 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)?
  • 有多少个(How Many)任务能并发执行?
  • 在队列中有多少个(How Many)任务在等待执行?
  • 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝?
  • 在执行一个任务之前或之后,应该进行哪些(What)动作?

各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。通过限制并发任务的数量,可以确保应用程序不会由于资源耗尽而失败,或者由于在稀缺资源的上发生竞争而严重影响性能。通过将任务的提交与任务的执行策略分离开来,有助于在部署阶段选择与可用硬件资源最匹配的执行策略。

每当看到下面这种形式的代码时:
new Thread(runnable).start()
并且你希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread。

6.2.3 线程池

线程池,从字面含义来看,是指管理一组同构工作线程的资源池。线程池是与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务。工作者线程的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

“在线程池中执行任务“比”为每个任务分配一个线程“优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而延迟任务的执行,从而提高了响应性。通过适当调整线程池的大小,可以创建足够多得线程以便使处理器保持忙碌状态,同时还可以方式过多线程相互竞争资源而使应用程序耗尽内存或失败。

类库提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors中的静态工厂方法之一来创建一个线程池:
newFixedThreadPool。newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。
newCachedThreadPool。newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
newSingleThreadExecutor。newSingleThreadExecutor是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。newSingleThreadExecutor能够确保依照任务在队列中的顺序来串行执行(例如FIFO、LIFO、优先级)。
newScheduledThreadPool。newScheduledThreadPool创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

newFixedThreadPool与newCachedThreadPool两个工厂方法返回通用的ThreadPoolExecutor实例。直接使用ThreadPoolExecutor,可以直接用来构造专门用途的Executor。

从”为每个任务分配一个线程“策略变成基于线程池的策略,将对应用程序的稳定性产生重大的影响:Web服务器不会再在高负载下失败(尽管服务器不会因为创建了过多的线程而失败,但在足够长的时间内,如果任务到达的速度总是超过任务执行的速度,那么服务器仍然有可能(只是更不易)耗尽内存,因为等待执行的Runnable队列将不断增长。可以通过使用一个有界的工作队列在Executor框架内部解决这个问题)。由于服务器不会创建数千个线程来争夺有限的CPU和内存资源,因此服务器的性能将平缓地降低。通过使用Executor,可以实现各种调优、管理、监视、记录日志、错误报告和其他功能。

6.2.4 Executor的生命周期

JVM只有在所有(非守护)线程全部终止后才会退出。因此,如果无法正确地关闭Executor,那么JVM将无法结束。

由于Executor以异步方式来执行任务,因此在任何时刻,之前提交的任务的状态不是立即可见的。有些任务可能已经完成,有些可能正在运行,而其他的任务可能在队列中等待执行。当关闭应用程序时,可能采用最平缓地关闭方式(完成所有已经启动的任务,并且不再接受任何新的任务),也可能采用最粗暴的关闭方式(直接关掉机房的电源),以及其他各种可能的方式。

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

public interface ExecutorService extends Executor {    void shutdown();    List<Runnable> shutdownNow();    boolean isShutdown();    boolean isTerminated();    boolean awaitTermination(long timeout, TimeUnit unit)        throws InterruptedException;    //  ... 其他用于任务提交的便利方法}

ExecutorService的生命周期有3种状态:运行、关闭和已终止。ExecutorService在初始创建时处于运行状态。shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成——包括那些还未开始执行的任务。shutdownNow方法将执行粗暴地关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。

在ExecutorService关闭后提交的任务将由”拒绝执行处理器“来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务都完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已经终止。通常在调用awaitTermination之后会立即调用shutdown,从而产生同步地关闭ExecutorService的效果。

下面LifecycleWebServer为Web Server提供了生命周期的支持进行了扩展,它支持两种关闭方式:通过编程手工的调用stop方法,另一种就是由客户端发送一个关闭请求然后再调用stop进行关闭。

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 (RejectedExecutionException 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            dispatchRequest(req);    }}

6.2.5 延迟任务与周期任务

Timer类负责管理延迟任务(”在100ms后执行该任务“)以及周期任务(”每10ms执行一次该任务“)。然而,Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它。可以通过ScheduleThreadPoolExecutor的构造函数或newScheduledThreadPool工厂方法来创建该类的对象。

Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。例如某个周期TimerTask需要每10ms执行一次,而另一个TimerTask需要执行40ms,那么这个周期任务或者在40ms任务执行完成后快速连续地调用4次,或者彻底”丢失“4次调用(取决于它是局域固定速率来调度还是基于固定延时来调度)。线程池能弥补这个缺陷,它可以提供多个线程来执行延迟任务和周期任务。

Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不捕获异常,因此当TimerTask抛出未检查的异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误地认为整个Timer都被取消了。因此,已经被调度但尚未执行的TimerTask将不会再执行,新的任务也不能被调度。(这个问题称之为”线程泄漏“)

由于Timer是启动一个线程来串行的从任务队列中取任务然后执行,如果任务抛出未检查异常,则这个异常不会被抛到调用Timer的线程,因为异常是不会从子线程抛到他的父线程中去的,所以只是Timer线程死掉,而它的父线程还会正常运行。只要某个任务抛出了未检测异常,如果你在计划任务将会得到一个携带“Timer already cancelled”信息的异常。但ScheduledThreadPoolExecutor能正确处理这些表现出错误行为的任务。在Java5.0或更高的JDK中奖很少使用Timer。

如下程序所示:

import static java.util.concurrent.TimeUnit.SECONDS;import java.util.Timer;import java.util.TimerTask;public class OutOfTime{    public static void main(String[] args) throws Exception    {        Timer timer = new Timer();        timer.schedule(new ThrowTask(), 1);        SECONDS.sleep(1);        timer.schedule(new ThrowTask(), 1);        SECONDS.sleep(5);    }    static class ThrowTask extends TimerTask    {        public void run()        {            throw new RuntimeException();        }    }}

抛出如下异常:

Exception in thread "Timer-0" java.lang.RuntimeException    at OutOfTime$ThrowTask.run(OutOfTime.java:21)    at java.util.TimerThread.mainLoop(Timer.java:555)    at java.util.TimerThread.run(Timer.java:505)Exception in thread "main" java.lang.IllegalStateException: Timer already cancelled.    at java.util.Timer.sched(Timer.java:397)    at java.util.Timer.schedule(Timer.java:193)    at OutOfTime.main(OutOfTime.java:13)

如果你要自己创建调度服务,你可以使用类库中提供的DelayQueue,它是BlockingQueue的一个实现,是它为ScheduledThreadPoolExecutor提供了调度的功能。DelayQueue是管理实现Delayed接口的对象,只有在延迟期满时才能从中提取元素,该队列的头部是延迟期满后保存时间最长的 Delayed 元素。实现Delayed的getDelay方法可以告诉DelayQueue所剩余延迟时间(零或负值指示延迟时间已经用尽,表示延时期满),只有延时期满的对象才能放入队列中,在调用take时会调用getDelay进行判断是否延时期满。在放入队列过程中还会根据延时长短进行排序,排序的依据是compareTo(因为Delayed还实现了Comparable接口)方法。

6.3 找出可利用的并行性

Executor框架帮助制定执行策略,但如果要使用Executor,必须将任务表述为一个Runnable。在大多数服务器应用程序中都存在一个明显的任务边界:单个客户请求。但有时候,任务边界并非是显而易见的,例如在很多桌面应用程序中。即使是服务器应用程序,在单个客户请求中仍可能存在可发掘的并行性,例如数据库服务器。

下面我们开发一个组件的不同版本,每个版本允许的不同的并发性。示例是模拟浏览器渲染HTTP页面的功能,模拟一个页面渲染器组件。假设HTTP页面里只有文本标签,当然在文本中穿插着图片(图片标签还带有尺寸大小)。

6.3.1 示例:串行地页面渲染器

最简单的方法就是对HTML文档进行串行处理。当遇到文本标签时,将其绘制到图像缓存中。当遇到图像引用时,先通过网络获取它,然后再将其绘制到图像缓存中。程序只需将输入中的每个元素处理一次(甚至不需要缓存文档),如果页面很大,图片很的多的情况下会让用户等等很长时间。

另一种串行执行方法更好一些,它先绘制文本元素,同时为图像预留出矩形的占位空间,在处理完了第一遍文本后,程序再开始下载图像,并将它们绘制到对应的占位空间中。SingleThreadRenderer就采用了这种方式:

public class SingleThreadRenderer {    void renderPage(CharSequence source) {        renderText(source);//渲染文本,并预留图像占位框        List<ImageData> imageData = new ArrayList<ImageData>();        for (ImageInfo imageInfo : scanForImageInfo(source))//扫描所有图像            imageData.add(imageInfo.downloadImage());        for (ImageData data : imageData)            renderImage(data);//下载并渲染图像    }}

图像下载过程的大部分时间都是在等待I/O操作执行完成,在这期间CPU几乎不做任何工作。因此,这种串行执行方法没有充分地利用CPU,使得用户在看到最终页面之前要等待过长的时间。通过将问题分解为多个独立的任务并发执行,能够获得更高的CPU利用率和响应灵敏度。

6.3.2 携带结果的任务Callable与Future

Executor框架使用Runnable作为其基本的任务表示形式。Runnable是一种有很大局限的抽象,虽然run能写入到日志文件或者将结果放入某个共享的数据结构,但Runnable不具有返回值与抛出受检查异常。

Callable:它认为主入口点(即call)将返回一个值,并可能抛出一个异常。

如果Callable不需要返回值,可以使用Callable<void>。

在Executor中包含了一些辅助方法能将其他类型的任务封装为一个Callable,例如Runnable和java.security.PrivilegedAction。

Runnable和Callable描述的都是抽象的计算任务。这些任务通常是有范围的,即都有一个明确地起始点,并且最终会结束。Executor执行的任务有4个生命周期:创建、提交、开始和完成。由于有些任务可能要执行很长的时间,因此通常希望能够取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们能响应中断时,才能取消。取消一个已经完成的任务不会有任何影响。

Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在Future规范中包含的隐含意义是,任务的生命周期只能前进,不能后退,就像ExecutorService的生命周期一样。当某个任务完成后,它就永远停留在“完成”状态上。下面是Future与Callable接口:

public interface Callable<V> {    V call() throws Exception;}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;}

在Executor框架中,总可以取消(shutdown、shutdownNow)已经提交但未开始的任务,但是对于已经开始执行的任务,只有它们响应中断才可以取消。取消已经完成的任务没有影响。

Future描述了任务的任命周期,并提供了相关方法来获取结果(get)、取消任务(cancel)以及检查任务是已经完成(isDone)还是被取消(isCancelled)。

Future任务生命周期是单向的,不能向后转换——与ExecutorService的生命周期一样,一旦任务,它就永远停留在完成状态上。

get方法的行为取决于任务的状态(尚未开始、正在运行、已完成)。如果任务已经完成,那么get会立即返回或者抛出一个Exception,如果任务没有完成,那么get将阻塞并直到任务完成。如果任务跑出了异常,那么get将该异常封装为ExecutionException并重新抛出。如果任务被取消,那么get将抛出CancellationException。如果get爬出了ExecutionException,那么可以通过getCause来获得被封装的初始异常。下面是get的源码片段:

V innerGet() throws InterruptedException, ExecutionException {    acquireSharedInterruptibly(0);//如果未执行完会在这里阻塞    if (getState() == CANCELLED)//如果任务已取消        throw new CancellationException();    if (exception != null)//如果任务执行的过程中抛出异常        throw new ExecutionException(exception);    return result;}

可以通过许多种方法创建一个Future来描述任务。ExecutorService中所有submit方法都将返回一个Future,从而将一个Runnable或Callable提交给Executor,并得到一个Future用来获得任务的结果或者取消任务。还可以显式地为某个指定的Runnable或Callable实例化一个FutureTask。(由于FutureTask实现了Runnable,因此可以将它提交给Executor来执行或者直接调用它的run方法(将它包装成Thread后当作线程执行,因为FutureTask具有Future特性,所以已可取消或获取执行结果))

从Java6开始,ExecutorService实现可以改写AbstractExecutorService中的newTaskFor方法,从而根据已提交的Runnable或Callable来控制Future的实例化过程。在默认实现中仅创建了一个新的FutureTask,如下所示:

protected <T> RunnableFuture<T> newTaskFor(Callable<T> task){    return new FutureTask<T>(task);}

将Runnable或Callable对象提交到Executor是一个对象安全发布的过程,即Runnable或Callable对象从提交线程传递到任务执行的任务的过程是线程安全的,我们不用担心这些对象是否完全初始(Runnable与Callable对象的工作内存中的数据写回到主内存中)。类似地,通过Future的get方法获取任务执行结果也是一个安全发布的对象,即将这个结果从计算它的线程发布到任何通过get获得它的线程。

6.3.3 示例:使用Future实现页面渲染

为了使页面渲染器实现更高的并发性,首先将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图像。(因为其中一个任务是CPU密集型,而另一个任务是I/O密集型,因此这种方法即使在单CPU系统上也能提升性能。)

使用Future等待图像下载:

public class FutureRenderer {    private final ExecutorService executor = ...;    void renderPage(CharSequence source) {        final List<ImageInfo> imageInfos = scanForImageInfo(source);        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;//返回下载的图片                    }                };        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());        }    }}

Future.get的异常处理代码将处理两个可能的问题:任务遇到了一个Exception,或者调用get的线程在获得结果之前被中断。

虽然上面在一定程度上提高了并发性,然后,我们还可以做得更好,用户不必等到怕有的图像下载完成后一下子看到所有图片,他们或许更希望只要下载完一幅图像就要看到一幅。

FutureRenderer用到了两个任务:一个负责渲染文本,一个负责下载图像。如果渲染文件的速度远远大于下载图像的速度(这完全是有可能的),那么最终的性能与顺序执行版的性能不会有很大的不同,反倒提高了代码的复杂度。

6.3.4 在异构任务并行化中存在的局限

在上个示例中,我们尝试并行地执行两个不同类型的任务——下载图像与渲染页面。然而,通过对异构任务进行并行化来获得重大的性能提升是很困难的。

两个人可以很好地分担洗碗的工作:其中一个人负责清洗,而另一个人负责烘干。然而,要将不同类型的任务平均分配给每个工人却并不容易。当人数增加时,如何确保它们能帮忙而不是妨碍其他人工作,或者在重新分配工作时,并不是容易的事情。如果没有在相似的任务之间找出细粒度的并行性,那么这种方法带来的好处将减少。

当在多个工人之间分配异构的任务时,还有一个问题就是各个任务的大小可能完全不同。如果将两个任务A和B分配给两个工人,但A的执行时间是B的10倍,那么整个过程也只能加速9%。最后,当在多个工厂之间分解任务时,还需要一定的任务协调开销:为了使任务分解能提高性能,这种开销不能高于并行性实现的提升。

FutureRenderer使用了两个任务,其中一个负责渲染文本,另一个负责下载图像。如果渲染文本的速度远远高于下载图像的速度(可能性很大),那么程序的最终性能与串行执行时的性能差别不大,而代码却变得更复杂了。当使用两个线程时,至多能将速度提高一倍。因此,虽然做了许多工作来并发执行异构任务以提高并发度,但从中获得的并发性却是十分有限的。

只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。

6.3.5 CompletionService:Executor与BlockingQueue

如果将Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后反复使用get方法,同时将参数timeout指定为0,从而通过轮询来判断任务是否完成。这种方法虽然可行,但却有些繁琐。幸运的是,还有一种更好地方法:完成服务(CompletionService)。

如果你向Executor提交了一批任务并且还要处理这批任务返回的结果,我们可以通过Executor的实现类ExecutorService的“List

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);    }    ...}

ExecutorCompletionService的使用大致如下:

//包装一个ExecutorExecutorCompletionService service = new ExecutorCompletionService(executor);//在循环中一个个提交任务,因为CompletionService不像ExecutorService具//有任务批量提交方法invokeAllfor (Callable<Integer> task : tasks) service.submit(task);//在循环中从队列中获取任务结果,结果的个数就是任务的个数,taks为任务集合for (int i = 0; i < taks.size(); i++)   count += service.take().get();//从阻塞队列中取出Futur,再读取结果,这里的结果肯定是执行完或取消的任务,调用get是不会阻塞的,但take是可能被阻塞的。

6.3.6 示例:使用CompletionService实现页面渲染器

可以通过CompletionService从两个方面来提升页面渲染器的性能:缩短总运行时间以及提高响应性。为每一幅图像的下载都创建一个独立的任务,并在线程池中执行它们,从而将串行地下载过程转换为并行的过程:这将减少下载所有图像的总时间。此外,通过从CompletionService中获取结果以及使每张图片在下载完成后立刻显示出来,能使用户获得一个更加动态和更高响应性的用户界面。如下程序所示:

public class Renderer {       private final ExecutorService executor;       Renderer(ExecutorService executor) {              this.executor = executor;       }       void renderPage(CharSequence source) {              final List<ImageInfo> info = scanForImageInfo(source);              //对ExecutorService进行包装,转换成CompletionService              CompletionService<ImageData> completionService =                     new ExecutorCompletionService<ImageData>(executor);              for (final ImageInfo imageInfo : info)//对图像信息循环                     //每幅图像一个下载任务                     completionService.submit(new Callable<ImageData>() {                            public ImageData call() {                                   return imageInfo.downloadImage();                            }                     });              //当所有图像下载任务启动后进行文本渲染工作              renderText(source);              try {                     //返回结果数目与任务数相等,对结果进行遍历                     for (int t = 0, n = info.size(); t < n; t++) {                            //从结果队列中获取Future结果对象,来一个就取一个,但这里有可能阻塞                            Future<ImageData> f = completionService.take();                            //获取任务结果,这里不可能被阻塞                            ImageData imageData = f.get();                            renderImage(imageData);//开始渲染图像                     }              } catch (InterruptedException e) {                     Thread.currentThread().interrupt();              } catch (ExecutionException e) {                     throw launderThrowable(e.getCause());              }       }}

多个ExecutorCompletionService可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个公共Executor的ExecutorCompletionService。

6.3.7 为任务设置时限

有时候,如果某个任务无法在指定的时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。例如,某个Web应用程序从外部的广告服务器上获取广告信息,但如果该应用程序在两秒内得不到响应,那么将显示一个默认的广告,这样即使不能获得广告信息,也不会降低站点的响应性能。类似地,一个门户网站可以从多个数据源并行地获取数据,但数据只会在指定的时间内等待数据,如果超出了等待时间,那么只显示已经获得的数据。

在有限时间内执行任务的主要困难在于,要确保得到答案的时间不会超过限定的的时间,或者在限定的时间内无法获得答案。在支持时间限制地Future.get中支持这种需求:当结果可用时,它将立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。

在使用显示任务时需要注意,当这些任务超时后应该立即停止,从而避免为继续计算一个不再使用的结果而浪费计算资源。要实现这个功能,可以由任务本身来管理它的限定时间,并且在超时后终止执行或取消任务。此时可再次使用Future,如果一个限时的get方法抛出了TimeoutException,那么可以通过Future来取消任务。如果编写的任务是可取消的,那么可以提前中止它,以免消耗过多的资源。

下面演示了限时的Future.get的一种典型应用,在指定时间内获取广告信息,如果get超时,会取消获取广告的任务,并使用默认信息代替:

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);//在限时内获取结果    } catch (ExecutionException e) {        ad = DEFAULT_AD;//如果广告任务执行失败,则设置默认广告    } catch (TimeoutException e) {        ad = DEFAULT_AD; //如果广告下载超时,则设置默认广告        f.cancel(true);//并取消广告下载任务    }    page.setAd(ad);//为页面设置广告    return page;}

上面是单个任务采用Future.get来限时的,如果现在我们要对一批任务进行限时,且我们不关心任务是否全部完成,则我们可以通过Executor的invokeAll、invokeAny的限时版本来提交任务。invokeAll会在指定的时间内一定会返回Future列表,但列表里可能会有被取消的任务;invokeAny只要有一个任务完成。

小结

通过围绕任务执行来设计应用程序,可以简化开发过程,并有助于实现并发。Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大的好处,必须定义清晰的任务边界。某些应用程序中存在着比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性。

方法小结

Future的get、cancel、isCancelled、isDone方法

get:在任务完成前一直阻塞。会抛出三种异常:CancellationException - 如果计算被取消、ExecutionException - 如果计算抛出异常、InterruptedException - 如果当前的线程在等待时被中断。

get(long timeout, TimeUnit unit):在超时之前且任务未完成则一直阻塞。除抛出以上三种异常

cancel(boolean mayInterruptIfRunning):试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用cancel时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则mayInterruptIfRunning参数决定了是否调用运行任务的线程的interrupt操作。

isCancelled:如果在任务正常完成前将其取消,则返回true

isDone:正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true
ExecutorService的submit、invokeAll、invokeAny方法

ExecutorService的有三个重载的submit方法:

1、 可以接收Runnable或Callable类型的任务,返回Future<?>类型的Future的get返回null。
2、 这三个方法都将提交的任务转换成了Future的实现类FutureTask实例,并作为submit的返回实例。
3、 另外调用这三个方法不会阻塞,不像invokeAll那样要等到所有任务完成后才返回,与不像invokeAny那样要等到有一个任务完成后才返回Future。
4、 这个三方法会调用Executor的execute来完成,因为Executor的execute会抛出RejectedExecutionException - 如果不能接受执行此任务、NullPointerException - 如果命令为 null这两个运行进异常,所以这三个方法也会抛出这两个异常。

T invokeAny(Collection<Callable<T>> tasks):
1、 只要某个任务已成功完成(也就是未抛出异常,这与任务完成概念不一样:任务完成是指定Future的isDone返回true,有可能是抛出异常后进行完成状态),才返回这个结果。一旦正常或异常返回后,则取消尚未完成的任务(即任务所运行的线程处理中断状态,一旦在它上面出现可中断阻塞的方法调用,则会抛出中断异常)。
2、 此方法会阻塞到有一个任务完成为止(正常完成或异常退出)。
3、 也是调用Executor的execute来完成
4、 调用get不会阻塞

invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit):
1、 只要在给定的超时期满前某个任务已成功完成(也就是invokeAny方法不能抛出异常,包括Future.get所抛的异常),则返回其结果。一旦正常或异常返回后,则取消尚未完成的任务。
2、 此方法会阻塞到有一个任务完成为止(正常完成或异常退出)。
3、 也是调用Executor的execute来完成
4、 调用get不会阻塞

List<Future<T>> invokeAll(Collection<Callable<T>> tasks):
1、 只有当所有任务完成时,才返回保持任务状态和结果的 Future 列表。返回列表的所有元素的 Future.isDone() 为 true。注意,可以正常地或通过抛出异常来已完成任务。
2、 此方法会阻塞到所有任务完成为止(正常完成或异常退出)。
3、 也是调用Executor的execute来完成,如果任务执行过程中抛出了其他异常,则方法会异常退出,且取消所有其他还未执行完成的任务。
4、 返回的列表中的Future都是已经完成的任务,get时不会再阻塞

invokeAll(Collection<Callable<T>> tasks, long timeout, TimeUnit unit):
1、 当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表(如果是超时返回的列表,则列表中的会包括这些还未执行完的任务,使用get获取结果时可能会抛出CancellationException异常)。返回列表的所有元素的 Future.isDone() 为 true。一旦返回后,即取消尚未完成的任务。注意,可以正常地或通过抛出异常来完成任务。
2、 此方法会阻塞到所有任务完成为止(正常完成或异常退出或超时)。
3、 也是调用Executor的execute来完成,如果任务执行过程中抛出了其他异常,则方法会异常退出,且取消所有其他还未执行完成的任务。
4、 返回的列表中的Future中会有因超时执行任务时异常而未执行完的任务,get时会抛出CancellationException或ExecutionException,当然所有的Future的get也不会阻塞。

参考:Java并发编程

1 0