java并发编程实践学习(6 )任务执行

一.在线程中执行任务

围绕执行任务来管理应用程序时,首先要清晰任务边界。理想情况下,任务是独立的活动:它的工作并不依赖于其他任务的状态,结果,或者边界效应。独立有利于并发性,如果能得到相应的处理器资源,独立的任务可以并行执行。
在正常的负载下,服务器应用程序应该兼具良好的吞吐量和快速的响应性。应用程序应该在负荷过载时平缓的劣化,而不是负载一高就简单的任务失败。所以我们要一个清晰的任务边界,并配合一个明确的任务执行策略。
服务器的一般任务边界:单独的客户请求,Web服务器,邮件服务器,文件服务器,EJB容器和数据库服务器。将独立的请求作为任务边界可以让任务兼顾独立性和适当的大小。

1.顺序的执行任务

应用程序内部的任务调度可能有多种策略,这些策略可以在不同程度上发挥出潜在的并发性。其中最简单的是单一的线程中顺序的执行。
但是顺序化处理几乎不能为服务器应用程序提供良好的吞吐量或快速的响应性。不过也有少数特例–比如任务的数量少但是生命周期长,或者服务器只能服务于唯一用户,服务器只能在同一个时间能处理同一个请求。

2.显式的为任务创建线程

为了更好的响应性,可以为每个服务请求创建一个新线程。
WebServer为每一个请求启动一个新的线程

class ThreadTaskWebServer{
    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.无限制创建线程的缺点

每任务每线程方法存在缺陷,尤其需要创建大量线程

  • 线程生命周期的开销。创建和销毁线程需要消耗大量的计算资源。
  • 资源消耗量。如果可运行的线程数多于可用处理器数,线程将空闲,会占用大量内存,给垃圾回收带来压力。竞争CPU时还会带来其他开销,如果有足够多的线程保持所有CPU忙碌,更多的线程会有百害而无一利。
  • 稳定性。 应该限制可创建线程的数量,根据不同的平台和JVM的启动参数,Thread的构造函数中请求栈的大小和底层操作系统的限制。如果超出限制会受到OutOfMemoryError限制。
    在一定范围内,增加线程可以提高系统的吞吐量,一旦超出范围更多的线程会让程序崩溃。

二.Executor框架

线程池为线程管理提供了同样的好处。作为Executor框架的一部分,java.util.concurrent提供了一个灵活的线程池实现。
Executor是一个简单接口,它可以用于异步执行任务,有多种不同的执行策略,他还为任务提交和任务执行之间的解耦提供了标准的方法,为使用Runnable描述任务提供了通用的方式,还提供对生命周期的支持以及钩子函数,可以添加统计收集,应用程序管理和监视器等拓展。
Executor基于商场消费者模式。提交任务的执行者是消费者,执行任务的线程时消费者,

1.使用Executor实现的Web Server

使用线程池的WebServer

class TaskExecutionWebServer{
    private static final int NTHREADS = 100;
    private static Executor exec = Executor.newFixedThreadPool(NTHREADS);

    public static void main(String[] args) throws IOException{
        ServerSocket socket = new ServerSocket(80);
        while(true){
            final Socekt connection = socket.accept();
            Runnable task = new Runnable(){
                public void run(){
                    handleRquest(connection);
                }
            };
            exec.execute(task);
        }
    }
}

2.执行策略

将任务的提交与任务的执行进行解耦,可以简单得为一个类的任务制定执行策略,并保证后续的修改不至于太困难。
一个执行策略指明了任务执行的“ what、where、when、how”:

  • 任务在什么(what)线程中执行 。
  • 任务以什么(what)顺序执行(FIFO、LIFO、优先级)?
  • 可以有多少(how many)个任务和并发执行?
  • 可以有多少个(how many)任务进入等待执行队列?
  • 如果系统过载,需要放弃一个任务,应该挑选哪一个(which)?另外怎么(how)通知应用程序知道。
  • 在一个任务执行前和后,应该做什么(what)处理?
    执行策略是资源的管理工具。最佳的执行策略取决于可用的计算机资源和你对服务质量的需求。

3.线程池

线程池管理是一个工作者线程的同构池。线程池是与工作队列精密绑定的。
工作队列就是持有等待执行的任务。
线程池中执行多任务线程有很多优势。重用线程而不是创建新的线程可以抵消创建销往线程的开销。在请求到达时工作线程就已经存在,提高了响应性。调整线程池的大小可以保持处理器忙碌。还可以防止过多的线程互相竞争资源。
你可以调用Executors中的静态工厂方法来创建线程。
newFixedThreadPool创建一个定长的线程池。提交任务达到最大长度后不再变化。
newCachedThreadPool创建一个可以缓存的线程池,如果线程池的长度超过了处理的需要,他可以灵活的回收空闲的线程。当需求增加时,它可以灵活的添加新的线程,并且不会对池的长度做任何限制。
newSingleThreadExecutor创建一个单线程化的executor,它只创建唯一的工作者来执行任务,如果这个线程异常结束,会有另一个取代他。executor会保证依照任务队列所规定的顺序(FIFO、LIFO、优先级)执行。
newScheduledThreadPool创建一个定长的线程池,而且支持定时的以及周期性的执行任务类似于Timer。
newFixedThreadPool和newSingleThreadExecutor可以创建通用的ThreadPoolExecutor,直接使用ThreadPoolExecutor可以创建满足自己需求的Executor。

4.Executor的生命周期

JVM在全部线程终止后才会退出。如果无法正确关闭Executor会阻止JVM的结束。
ExecutorService中的生命走起方法

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

ExecutorService暗示了生命周期有三种状态:运行(running)、关闭(shutting down)、和终止(terminated)。最初被创建的时候是运行状态,shutdown方法会启动一个平稳的关闭过程:停止接受新的任务,同时等待已经提交的任务完成(包括尚未开始的任务)。shoutdownNow方法会启动一个强制关闭的过程:尝试取消所有运行中的任务和排在队列中尚未开始的任务。
关闭后提交到ExecutorService 中的任务会被拒绝执行处理器处理。拒绝执行处理器可能只是简单的放弃任务,可能也会引起execute抛出一个未检查的RejectedExecutionException、一旦所有任务完成后Executor会转入终止状态。可以调用awaitTerminal等待到达终止。也可以轮训检查isTerminated判断是否终止。通常shutdown紧随在awaitTermination之后可以同步关闭ExecutorService。

5.延迟的、并具周期性的任务

Timer工具管理任务的延迟执行以及周期性执行。但是timer存在一些缺陷,因此你可以考虑使用ScheduledThreadPoolExecutor作为代替品。你可以通过构造函数或者通过newScheduledThreadPool工厂方法,创建一个ScheduledThreadPoolExecutor。
Timer只创建唯一的一个线程来执行所有的timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出现问题。调度线程池解决了这个缺陷,他让你可以提供多个线程来执行延迟、并具周期性的任务。Timer的另一个问题在于,如果TimerTask抛出未检查异常,会终止timer线程。这种情况下,Timer也不会再重新恢复现成的执行。它认为整个Timer都被取消了。(这个问题叫做线程泄露)
在java5.0以后几乎没有理由使用Timer
如果你需要构建自己的调度服务,仍然可以使用类库提供的DelayQueue,它是BlockingQueue实现,为ScheduledThreadPoolExecutor提供了调度功能。DlayQueue管理着一个包含Delayed对象的容器。每个Delayed对象都与一个延迟时间相关联:只有元素过期后,DelayQueue才能让你执行take操作获取元素。从DelayQueue中返回的对象将根据它们所延迟的时间进行排序。

三.寻找可强化的并行性

Executor框架让制定一个执行策略变得简单。但你必须将任务描述为Runnable。但是大多数应用服务器还存在这样的请求:单一的客户请求。即使这样仍然会有进一步细化到并行性。

1.示例顺执行的页面渲染

处理HTML文档最简单的方法就是顺序处理。当遇到一个文本标签,就将它渲染到图像缓存里;当遇到一个图像引用时,先通过网络获取它,然后也将它渲染到图像缓存里。这很容易实现但是用户可能会等很长的时间。
另一种同样是顺序执行的方法可能会好一些,它先渲染文字,为图像留下矩形占位符,处理完文本后再处理图形。
但是下载图像总得等待I/O完成,这时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);
    }
}

2.可携带结果的任务:Callable和Future

Executor使用Runnable作为任务的基本表达形式,但是它不能返回一个值或者抛出受检查的异常。
很多任务都有严重的计算延迟-数据库查询,网络上获取资源,进行复杂计算。这时Callable是更好的:他在主进入点-call-等待返回值,并为可能抛出的异常预先做好了准备。Executors包含一些工具可以将其他任务类型封装成一个Callable。
Callable和Runnable描述的是抽象的计算任务。一个Executor执行任务的生命周期有:创建、提交、开始、完成。由于执行时间可能可能很长,我们希望可以取消任务。在Executor框架中,总可以取消已经提交但是未开始的任务。但是已经开始的任务只有响应中断才能取消。取消对已经完成的任务没有影响。
任务的状态决定了get方法的行为:

  • 如果任务已经完成,get会立即返回或者抛出一个exception。
  • 如果任务没有完成,get会阻塞直到它完成。
  • 如果任务抛出了异常,get会将该异常封装为ExecutionException然后抛出,可以用getCause获取被封装的原始异常。
  • 如果任务被取消,get会抛出CancellationException。

ExecutorService中所有submit方法都返回一个Future。因此你可以将一个Runnable或者Callable提交给executor,然后得到一个Furure。用它来重新获取任务的执行结果或者取消任务。也可以显示的给Runnable或者Callable实例化一个FutureTask(FutureTask实现了Runnable,所以既可以将它提交给Executor来执行,也可以直接调用run方法运行)

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

为了使页面渲染器实现更高的并发性,首先将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图像(一个是CPU密集型,一个是I/O密集型)。Callable和Future有助于表示这种协同任务的交互,以下代码首先创建一个Callable来下载所有的图像,当主任务需要图像时,它会等待Future.get的调用结果。如果幸运的话,图像可能已经下载完成,即使没有,至少也已经提前开始下载。

public class FutureRenderer {
    private final ExecutorService executor = Executors.newCachedThreadPool();

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

当然,我们还可以优化,用户其实不需要等待所有图像下载完成,我们可以每下载完一张图像就立刻显示出来。

4.并行运行异类任务的局限性

当我们试图将不同的任务分配给不同的线程执行时,各个任务的大小可能完全不同,可能其中一个线程很快的完成而另一个很慢。这样对性能的提升并不是很高,却使代码的复杂度大大提高。
大量同类的任务进行并发处理,会将程序的任务量分配到不同的任务中,这样才能真正的获得性能的提升。

5.CompletionService:当Executor遇见BlockingQueue

CompletionService整合了Executor和BlockingQueue的功能,你可以将Callable任务提交给它执行,然后使用类似于队列中的take和poll方法,在结果完整可用时获得这个结果,像一个打包的Future.
ExecutorCompletionService是实现CompletionService接口的一个类,并将计算任务委托给一个Executor。它在构造函数中创建一个BlockingQueue,用它去保存完成的结果。计算完成时会调用Futuretask中的done方法。当提交一个任务后,首先吧任务包装为一个QueueingFuture,它是FutureTask的一个子类,然后覆写done方法将结果置入BlockingQueue中。

6.示例:使用CompletionService的页面渲染器

要实现下载完一张就立刻绘制,我们需要及时知道图片下载完成,对于这种场景,CompletionService十分符合需求。CompletionService将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者 submit 执行的任务,使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。
下面的代码使用CompletionService改写了页面渲染器的实现

public abstract class Renderer {
    private final ExecutorService executor;

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

    void renderPage(CharSequence source) {
        final List<ImageInfo> info = scanForImageInfo(source);
        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<ImageData> f = completionService.take();
                ImageData imageData = f.get();
                renderImage(imageData);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

7.为任务限时

有时候如果一个活动无法在某个确定的时间内完成,那么他的结果就是失败了,此时应该放弃该活动。Future.get的限时版本符合这个条件;它在结果准备好后立刻返回,如果在时限内没有准备好就会抛出TimeoutException。如果超时,你可以立刻停止任务,或者通过取消任务,这样不会为无用的任务继续浪费资源。为了完成这个需求,你可以在有限时的get抛出TimeoutException时通过Future取消任务。
在预定时间内获取广信息

Page renderPageWithAd() throws InterruptedException {
    long endNanos = System.nanoTime() + TIME_BUDGET;
    Future<Ad> f = exec.submit(new FetchAdTask());
    // Render the page while waiting for the ad
    Page page = renderPageBody();
    Ad ad;
    try {
        // Only wait for the remaining time budget
        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;
}

8.示例:旅游预订门户网站

在预定时间内请求旅游报价

public class QuoteTask implements Callable<TravelQuote>{
    private final TravelCompany company;
    private final TravelInfo travelInfo;
    ....
    public TravelQuote call() throws 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));

        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 {
                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;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章