并发——任务执行


1. 在线程中执行任务

在理想情况下,各个任务之间是互相独立的:任务并不依赖于其他任务,结果或边界效应。独立性有助于实现并发,因为如果存在足够多的处理资源,那么这些独立的任务都可以并行执行。为了在调度与负载均衡等过程中实现更高的灵活性,每项任务还应该表示应用程序的一小部分处理能力。

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

1.1 串行地执行任务

最简单的调度任务的策略就是在单个线程在串行地执行各项任务。


串行的Web服务器

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

对于在Web请求的处理中包含了一组不同的运算与I/O操作。服务器必须处理套接字I/O以读取请求和写回响应,这些操作通常会由于网络拥塞或连通性问题而被阻塞。在单线程服务器中,阻塞不仅会推迟当前请求的完成时间,而且还将彻底阻止等待中的请求被处理。

串行处理机制通常无法提供高吞吐率或快速响应性。但对于任务数量很少且执行时间很长时,或者当服务器只为单个用户提供服务,并且该客户每次只发出一个请求时,比较适用。

1.2 显式的为任务创建线程

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


在Web服务器中为每个请求启动一个新的线程(不要这么做)

public 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();
        }
    }
}

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

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

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

1.3 无限制创建线程的不足

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

线程生命周期的开销非常高。
资源消耗。
稳定性。

2.Executor框架

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

线程池简化了线程的管理工作,并且java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分。在Java类库中,任务执行的主要抽象不是Thread,而是Executor。


Executor接口

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

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

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

2.1 实例:基于Executor的Web服务器

固定长度的线程池,可容纳100个线程


基于线程池的Web服务器

public 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() {
                @Override
                public void run() {
                    handleRequest(connection);
                }
            };
            exec.execute(task);
        }
    }
}

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


为每个请求启动一个新线程的Executor

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

在调用线程中已同步方式执行所有任务的Executor

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

2.2 执行策略

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

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

2.3 线程池

线程池,是指管理一组同构工作线程的资源池。线程池是与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务,然后返回线程池并等待下一个任务。

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

类库提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors中的静态工厂方法之一来创建一个线程池:
newFixedThreadPool
new FixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这是线程池的规模将不再变化。

newCachedThreadPool
newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当前需求增加时,可以添加新的线程,线程池的规模不存在任何限制。

newSingleThreadExecutor
newSingleThreadExecutor是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程发生异常结束,会创建另一个线程来代替。newSingleThreadExecutor能确保依照任务在队列中的顺序来串行执行(例如FIFO,LIFO,优先级)。

newScheduledThreadPool
newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

newFixedThreadPoolnewCachedThreadPool
这两个工厂方法返回通用的ThreadPoolExecutor实例,这些实例可以直接用来构造专门用途的executor。
TaskExecutionWebServer
TaskExecutionWebServer中的Web服务器使用了一个带有有界线程池的Executor。通过execute方法将任务提交到工作队列中,工作线程反复地从工作队列中取出任务并执行它们。

通过使用Executor,可以实现各种调优,管理,监视,记录日志,错误报告和其他功能。

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

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


支持关闭操作的Web服务器

public 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() {
                    @Override
                    public void run() {
                        handleRequest(conn);
                    }
                });
            } catch (IOException 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);
    }
}

2.5 延迟任务与周期任务

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

Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。线程池能弥补这个缺陷,它可以提供多个线程来执行延时任务和周期任务。

Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不捕获异常,因此当TimerTask抛出为检查的异常时将终止定时线程。已经被调度但未执行的TimerTask将不会再执行,新的任务也不能被调度。(问题被称之为“线程泄露”)

如果要构建自己的调度服务,那么可以使用DelayQueue,它实现了BlockingQueue,并为ScheduledThreadPoolExecutor提供调度功能。DelayQueue管理着一组Delayed对象。每个Delayed对象都有一个响应的延迟时间:在DelayQueue中,只有某个元素逾期后,才能从Delayed中执行take操作。从DelayQueue中返回的对象将根据它们的延迟时间进行排序。

3. 找出可利用的并行性

3.1 实例:串行的页面渲染器

最简单的方法就是对HTML文档进行串行处理。当遇到文本标签时,将其绘制到图像缓存中。当遇到图像引用时,先通过网络获取它,然后再将其绘制到图像缓存中。这很容易实现,程序只需要将输入中的每个元素处理一次(甚至不需要缓存文档),但这种方法可能会令用户感到烦恼,他们必须等待很长时间,知道显示所有的文本。
另一种串行执行方法更好一些,他先绘制文本元素,同时为图像预留出矩形的占位空间,在处理完了第一遍文本后,程序在开始下载图像,并将它们绘制到相应的占位空间中。
图像下载过程的大部分时间都是在等待I/O操作执行完成,在这期间CPU几乎不做任何工作。因此,这种串行执行方法没有充分地利用CPU,使得用户在看到最终页面之前要等待过长的时间。通过将问题分解为多个独立的任务并发执行,能够获得更高的CPU利用率和响应灵敏度。


串行地渲染页面元素

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

3.2 携带结果的任务Callable与Future

许多任务实际上都是存在延迟的计算——执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务,Callable是一种更好的抽象:它认为主入口点(即call)将返回一个值,并可能抛出一个异常。在Executor中包含了一些辅助方法能将其他类型的任务封装为一个Callable,例如Runnable和java.security.PrivilegedAction。

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

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

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


Callable与Future接口

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

可以通过许多种方法创建一个Future来描述任务。ExecutorService中的所有submit方法,都将返回一个Future,从而将一个Runnable或Callable提交给Executor,并得到一个Future用来获得任务得执行结果或者取消任务。还可以显示地为某个指定的Runnable或Callable实例化一个FutureTask。


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

再将Runnable或Callable提交到Exector的过程中,包含了一个安全发布过程,即将Runnable或Callable从提交线程发布到最终执行任务的线程。

3.3 实例:使用Future实现页面渲染器

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

Callable和Future有助于表示这些协同任务之间的交互。


使用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>>() {
            @Override
            public List<ImageData> call() throws Exception {

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

FutureRenderer使得渲染文本任务与下载图像数据的任务并发执行。当所有图像下载完后,会显示到页面上。这将提升用户体验,不仅使用户更快看到结果,还有效利用了并行性,但我们还可以做得更好。用户不必等到所有的图像都下载完成,而希望看到每当下载完一幅图像时就立即显示出来。

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

如果没有在相似的任务之间找出细粒度的并行性,那么这种方式带来的好处将减少。
FutureRenderer使用了两个任务,其中一个负责渲染文本,另一个负责下载图像。如果渲染文本的速度远远高于下载图像的速度,那么程序的最终性能与串行执行时的性能差别不大,而代码却变得更加复杂了。
只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出程序的工作负责分配到多个任务中带来的真正性能提升。

3.5 CompletionService:Executor与BlockingQueue

CompletionService将Exector和BlockingQueue的功能融合在一起。你可以将Callable任务提交给它执行,然后使用类似与队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时将被封装为Future。ExectorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。


由ExecutorCompletionService使用的QueueingFuture类

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

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

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


使用CompletionService,使页面元素在下载完成后立即显示出来

public class Renderer {
    private final ExecutorService executor;

    Renderer(ExecutorService executor) {
        this.executor = executor;
    }

    void renderPage(CharSequence source) {
        List<ImageInfo> info = scanForImageInfo(source);
        CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor);
        for (final ImageInfo imageInfo : info) {
            completionService.submit(new Callable<ImageData>() {
                @Override
                public ImageData call() throws Exception {
                    return imageInfo.downloadImage;
                }
            });
            renderText(source);

            try {
                for (int t = 0, n = info.size(); t < n; t++) {
                    Future<ImageData> f = completionService.take();
                    ImageData imageData = f.get();
                    renderImage(imageInfo);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (ExecutionException e) {
                throw launderThrowable(e.getCause());
            }
        }
    }
}

多个ExecutorCompletionService可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个公共Executor的ExecutorCompletionService。因此,CompletionService的作用就相当于一组计算的句柄,这与Future作为单个计算的句柄是非常类似的。通过记录提交给CompletionService的任务数量,并计算出已经获得的已完成结果的数量,即使使用一个共享的Executor,也能知道已经获得了所有任务结果的时间。

3.7 为任务设置时限


在指定时间内获取广告信息

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 ppage;
}

3.8 示例:旅行预订门户网站

考虑这样一个旅行预定门户网站:用户输入旅行的日期和其他要求,门户网站获取并显示来自多条航线,旅店或汽车租赁公司的报价。可能会调用Web服务,访问数据库,执行一个EDI事物或其他机制。

在这种情况下,不宜让页面的响应时间受限于最慢的响应时间,而应该只显示在指定时间内收到的信息。对于没有及时响应的服务提供者,页面可以忽略或者显示提示信息。

从一个公司获取报价的过程与其他公司获得报价的过程无关,因此可以将获取报价的过程当成一个任务,从而使获得报价的过程能并发执行。创建n个任务,将其提交到线程池,保留n个Futrue,并使用限时的get方法通过Future串行地获取每一个结果,这一切都很简单,但我们还可以使用一个更简单的方法——invokeAll(invoke 援引)

下面的代码使用了支持显示的invokeAll,将多个任务提交到一个ExecutorService并获得结果。

InvokeAll方法的参数为一组任务,并返回一组Future。这两个集合有着相同的结构。
InvokeAll按照任务集合中迭代器额顺序肩所有的Future添加到返回的集合中,从而使调用者能将各个Future与其表示Callable关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll将返回。将超过指定时限后,任何还未完成的任务都会被取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,没有正在执行的任务,而客户端可以调用get或isCancelled来判断究竟是何种情况。

//           6-17     在预定时间内请求旅游报价
//           请求旅游报价的方法
private class QuoteTask implements Callable<TravelQuote>{
    private final TravelCompany company;
    private final TravelInfo travelInfo;

    public TravelQuote call()throw Exception{
        return company.solicitQuote(travelInfo);
    }
}

public List<TravelQuote> getRankedTravelQuotes(
     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));
             //InvokeAll方法的参数为一组任务,并返回一组Future ,用时限来限制时间       
             List<Future<TravelQuote>> futures =
                   exec.invokeAll(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 {
             //invokeAll按照任务集合中迭代器额顺序肩所有的Future添加到返回的集合中
                quotes.add(f.get());
             } catch (ExecutionException e) {
                quotes.add(task.getFailureQuote(e.getCause()));
             } catch (CancellationException e) {
                quotes.add(task.getTimeoutQuote(e));
             }
         }
              Collections.sort(quotes, ranking);
              return quotes;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值