《JAVA并发编程实战》任务执行

版权声明:本文为博主原创文章,转载请在明显的位置注明出处。如果觉得文章对你有用,给我点个赞呗~ https://blog.csdn.net/sayWhat_sayHello/article/details/83410067

在线程中执行任务

当围绕“任务执行”来设计应用程序结构时,第一步是找出清晰的任务边界。在理想情况下,各个任务之间是相互独立的。

在正常的负载下,服务器应用程序应该同时表现出良好的吞吐量和快速的响应性。应用程序提供商希望程序支持尽可能多的用户,从而降低每个用户的服务成本,而用户则希望获得尽快的响应。而且,当符合过载时,应用程序的性能应该是逐渐降低的,而不是直接失败。

大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。

串行执行任务

在应用程序中单个线程中串行的执行各项任务是最简单的策略。

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

执行性比较糟糕,因为一次只能处理一个请求。主线程在接受连接和处理相关请求等操作之间不断的交替运行。当服务器正在处理请求时,新到来的连接必须等到请求处理完成,然后服务器再次调用accept。

在服务器应用程序中,串行处理机制通常无法提供高吞吐率或快速响应性。

显式的为任务创建线程

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

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

和上面的区别在于,对于每个连接,主循环都将创建一个新线程来处理请求,而不是在主循环中进行处理。由此得出:

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

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

无限制创建线程的不足

在生产环境中,上述方法仍然存在一些缺陷:

  • 线程生命周期的开销非常高。线程创建和销毁都有代价,如果为每个请求创建一个新线程将销毁大量的计算资源。
  • 资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可以处理器的数量,那么有些线程将闲置。大量的空闲线程会占用许多内存。
  • 稳定性。在可创建线程的数量上存在一个限制。这个限制随着平台的不同而不同,并且受多个因素制约,包括JVM启动参数等。如果破坏了这些限制,那么很可能抛出OutOfMemoryError.

为“每个任务分配一个线程”的问题在于,它没有限制可创建线程的数量。

Executor框架

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

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

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

Executor基于生产者-消费者模式,提供任务的操作相当于生产者,执行任务的线程相当于消费者。

示例:基于Executor的Web服务器

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

在上述案例中,通过使用Executor将请求处理任务的提交和任务的实际执行解耦开来,并且只要常用另一种不同的Executor实现就可以改变服务器的行为。

“每次请求创建新线程”

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

类似单线程行为即以同步的方式执行每个任务

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

执行策略

通过将任务的提交和执行解耦,从而无须太大的困难就可以为某种类型的任务指定和修改执行策略。在执行策略中定义了任务执行的:

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

各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量需求。通过限制并发任务的数量,可用确保应用程序不会由于资源耗尽而失败,或者由于在稀缺资源上发生竞争而严重影响性能。

通过将任务的提交和任务的执行策略分离开来,有助于在部署阶段选择和可用硬件资源最匹配的执行策略。

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

线程池

管理一组同构工作线程的资源池。线程池和工作队列密切相关,其中在工作队列中保存了所有等待执行的任务。工作者线程(Worker Thread)的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

通过重用现有线程而不是创建新线程可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。另外一个好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。

类库中提供了一些Executor的创建方式:

  • newFixedThreadPool:创建固定长度的线程池,每当提交一个任务时创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化。
  • newCachedThreadPool:创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不受限制。
  • newSingleThreadExecutor:单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来代替。确保依照任务在队列中的顺序来串行执行。
  • newScheduledThreadPool:创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer

Executor的生命周期

Executor的实现通常会创建线程来执行任务。但JVM只有在所有(非守护)线程全部终止后才会退出。因此,如果无法正确的关闭Executor,你们JVM将无法结束。

由于Executor以异步方式来执行任务,因此在关闭应用程序时,可能采用最平缓的关闭形式(完成所有已启动的任务,并且不再接受任何新的任务),也可能是最粗暴的形式(停电),或者其他。既然Executor是为应用程序提供服务的,因此他们也是可以被关闭的,并将在关闭操作中受影响的任务的状态反馈给应用程序。

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

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout,TimeUnit unit) throws InterruptedExeception;
    //...
}

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

class LifeCycleWebServer {
    private int ExecutorService exec =  ...;
    
    public void start() throws IOExeception {
        ServerSocket socket = new ServerSocket(80);
        while(!exec.isShutdown()) {
            try {
                final Socket conn = socket.accept();
                exec.execute(new Runnable() {
                    public void run() {
                        handleRequest(conn);
                    }
                });
            } catch(RejectedExecutionExeception e) {
                if(!exec.isShutdown()){
                    log("task submission rejected",e);
                }
            }
        }
    }
    
    public void stop() {
        exec.shutdown();
    }
    
    void handleRequest(Socket conn) {
        Request req = readRequest(conn);
        if(isShutdownRequest(req)) {
            stop();
        }
        else {
            dispatchRequest(req);
        }
    }
    
}

延迟任务和周期任务

Timer类负责管理延迟任务(100秒后执行该任务)和周期任务(每10秒执行一次该任务),然而Timer存在一些缺陷,因此考虑使用ScheduledThreadPoolExecutor来代替它。

Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精准性。

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

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

public class OutOfTime {
    public static void main(Stirng[] args) throws Exeception {
        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 RuntimeExeception();
        }
    }
}

程序在1秒就终止了,并抛出一个异常“Timer already cancelled”

找出可利用的并行性

Executor框架帮助指定执行策略,但如果要使用Executor,必须将任务表述为一个Runnable.

示例:串行的页面渲染器

最简单的方法就是对HTML文档进行串行处理。当遇到文本标签时,将其绘制到图像缓存中。当遇到图像引用时,先通过网络获取它,然后再将其绘制到图像缓存中。
这种方式可能会让用户感到烦恼,因为他们必须等待很长时间,直到显示所有文本。

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

public class SingleThreadRenderer {
    void renderPage(CharSequence source) {
        renderText(source);
        List<ImageData> imageData = new ArrayList<>();
        for(ImageInfo imageInfo : scanForImageInfo(source)){
            imageData.add(imageInfo.downloadImage());
        }
        for(ImageData data : imageData) {
            renderImage(data);
        }
    }
}

携带结果的任务Callable和Future

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

Future表示一个任务的生命周期,并提供了相应的方法判断是否已经完成或取消,已经获取任务的结果和取消任务等。

public interface Callable<V> {
    V call() throws Exception;
}

public interface Futute<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.

从java6开始,ExecutorService实现可以改写AbstractExecutorService中的newTaskFor方法,从而根据已提交的Runnable或者Callable来控制Future的实例化过程。

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

通过Future实现页面渲染器

我们将渲染分为两个任务:

  1. 渲染所有文本
  2. 下载所有图像

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

public class FutureRenderer {
    private final ExecutorService executor = ...;
    
    void renderPage(CharSequence source) {
        final List<ImageInfo> imageInfos = scanForImageInfo(source);
        Callable<List<ImageData>> task = new Callable<>() {
            public List<ImageData> call(){
                List<ImageData> result = new ArrayList<>();
                for(ImageInfo imageinfo : imageInfos) {
                    result.add(imageInfo.downloadImage());
                }
            };
        }
        
        Future<ListImageData>> future = executor.submit(task);
        renderText(source);
        
        try {
            List<ImageData> imageData = future.get();
            for(ImageData data : imageData){
                renderImage(data);
            }
        } catch(InterriptedException e){
            Thread.currentThread().interrupt();
            future.cancel(true);
        } catch(ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

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

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

FutureRenderer使用了两个任务,其中一个负责渲染文本,另一个负责下载图像。如果渲染文本的速度远远高于下载图像的速度,那么程序的最终性能和串行执行的性能差别不大,而代码却变复杂了。

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

CompletionService:Executor和BlockingQueue

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

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

ExecutorCompletionService的实现非常简单。在构造函数中创建一个BlockingQueue来保存计算完成的结果。当计算完成时,调用Future-Task中的done方法。当提交某个任务时,该任务将首先包装成一个QueueingFuture,这是FutureTask的一个子类,然后再改写子类的done方法,并将结果放入BlockingQueue中。take和poll方法委托给了BlockingQueue,这些方法会在得到结果前阻塞。

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

示例:使用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>(){
                public ImageData call() {
                    return imageInfo.downloadImage();
                }
            });
        }
        
        renderText(source);
        
        try {
            for(int i = 0,n = info.size();t < n;t++){
                Future<ImageData> f = completinService.take();
                ImageData imageData = f.get();
                renderImage(imageData);
            }
        } catch(InterruptedException e) {
            Thread.currentThread().interrpt();
        } catch(ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

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

为任务设置时限

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

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

示例:旅行预定门户网站

invokeAll将多个任务提交到一个ExecutorService并获得结果。InvokeAll方法的参数为一组任务,并返回一组Future。invokeAll按照任务集合中迭代器的顺序间将所有的Future添加到返回的集合中,从而使调用者能将各个Future和其表示的Callable关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll将返回。当超过指定时限后,然后还未完成的任务都将会取消。当invokeAll返回后,每个任务要么正常完成,要么被取消,客户端代码可以调用get或者isCancelled判断是哪种情况。

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

private 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<>();
    for(TravelCompany company : companies) {
        tasks.add(new QuoteTask(company,travelInfo());
    }
    List<Future<TravelQuote>> futures = exec.invokeAll(tasks,time,unit);
    
    List<TravelQuote> quotes = new ArrayList<>(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;
}

猜你喜欢

转载自blog.csdn.net/sayWhat_sayHello/article/details/83410067