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

概述:

大多数并发应用程序都是围绕“任务执行”来构造的:任务通常是一些抽象的且离散的工作单元。通过把应用程序的工作分解到多个任务中,可以简化程序的组织结构,提供一种自然的事务边界来优化错误恢复过程,以及提供一种自然的并行工作结构来提升并发性。

1、在线程中执行任务

当围绕"任务执行"来设计应用程序结构时,第一步就是要找出清晰的任务边界。在理想情况下,各个任务之间是相互独立的:任务并不依赖于其他任务的状态、结果或者边界效应。独立性有助于并发,yi8nwei如果存在足够多的处理资源,那么这些独立的额任务都可以并行执行。为了在调度与负载均衡等过程中实现更高的灵活性,每项任务还应该表示应用程序的一小部分处理能力。
在正常的负载下,服务器应用程序应该同时表现出良好的吞吐量和快速的响应性。应用程序供应商希望程序支持尽可能多的用户,从而降低每个用户的生活成本,而用户则希望获得尽快的响应。而且,当负荷过载的时候,应用程序的性能应该是逐渐降低,而不是直接失败。
大多数服务器你应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。
1)串行地执行任务
简单,理论上正确,但生产中执行性能却很糟糕,因为每次只能处理一个请求。如果处理中包括了不同的运算与I/O操作。服务器必须处理套接字I/O以读取请求和回写响应。这些操作通常会由于网络阻塞或者连通性问题而被阻塞。在单线程的服务器中,阻塞不仅会推迟当前请求的完成时间,而且还将彻底阻止等待中的请求被处理。如果请求阻塞的时间过长,用户将认为服务器是不可用的呃,因为服务器看上去似失去了响应。
2)显式地为任务创建线程
对于每个连接,主循环都将创建一个新线程来处理请求,而不是在主循环中进行处理。由此得出:
任务处理过程从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接。这使得程序在完成前面的请求之前可以接受新的请求,从而提高响应性。
任务可以并行处理,从而能同事服务多个请求。如果有多个处理器,或者任务由于某种原因被阻塞,例如等待I/O完成、获取锁或者资源可用性等,程序的吞吐量将得到提高。
任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。
3)无限制创建线程的不足
为每个任务分配一个线程,这种方法存在一些缺陷,尤其是当需要创建大量的线程时。
线程生命周期的开销非常高。
资源消耗。火雨的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程将闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力。当你已经拥有足够多的线程使所有CPU保持忙碌状态,那么再创建更多的线程反而会降低性能。
稳定性。在可创建线程的数量上存在一个限制。这个限制值将随着平台的不同而不同,并且受多个因素制约,包括JVM的启动参数。Thread构造函数中请求的栈大小,以及底层操作系统对线程的限制等
package chapter6;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author zhen
 * @Date 2018/11/14 10:19
 * 串行的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);
        }
    }

    public static void handleRequest(Socket socket) {}
}
串行执行线程任务例子
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author zhen
 * @Date 2018/11/14 10:25
 * 在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() {
               @Override
               public void run() {
                   handleRequest(connection);
               }
           } ;
           new Thread(task).start();
        }
    }

    public static void handleRequest(Socket socket){}
}
为每个任务创建线程例子
package chapter6;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * @author zhen
 * @Date 2018/11/14 10:36
 * 基于线程池的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();
        while(true) {
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    handleRequest(connection);
                }
            };
            exec.execute(task);
        }
    }

    public static void handleRequest(Socket socket) {}
}
基于线程池的web服务器例子

 

2、Executor框架 

任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。两种通过线程来执行任务的策略:把所有任务放在单个线程中串行执行以及将每个任务放在各自的线程执行。但这两种方式都存在一些严格的限制:串行执行的问题在于对于其糟糕的响应性和吞吐量,而“为每个任务分配一个线程"的问题在于资源管理的复杂性。
前面介绍了使用有界队列防止高负荷的应用程序耗尽。线程池简化了线程的管理工作,并且java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分。在java类库中,任务执行的主要抽象不是Thread,而是Executor。
Executor接口:
public interface Executor {
void execute(Runnable command);
}
虽然Executor是个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable表示任务。Executor的实现还提供了对声明周期的支持,以及统计信息收集、用用程序管理机制和性能监视等机制。
1)基于Executor的Web服务器
2)执行策略
通过将任务的提交与执行解耦开来,从而无须太大的困难就可以为某种类型的任务指定和修改执行策略。包括:
在什么线程执行任务
任务按照什么顺序执行(FIFO,LIFO,优先级)?
有多少个任务能并发执行
在队列中有多少个任务在等待执行
如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个任务?另外,如何通知应用程序有任务被拒绝
在执行一个任务之前或之后,应该进行哪些动作?
当希望使用一种灵活的执行策略的时候请考虑使用Executor来代替Thread
3)线程池
从字面含义来看,是指管理一组同构工作线程的资源池。线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务。工作者线程(Worker Thread)的任务很简单:从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。
线程池中执行任务比为每个任务分配一个线程优势更多。通过重用现在的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程时中产生的巨大开销。另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
类库提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors中的静态工厂方法之一来创建一个线程池:
newFixedThreadPool。newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,知道达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。
newCachedThreadPool。newCachedThreadPool将创建一个可缓存的线程池,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。newSingleThreadExecutor能确保依照任务在队列中的顺序来串行执行。
newScheduledThreadPool。newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或者定时的方式来执行任务,类似于Timer。

4)Executor的生命周期
我们已经知道如何创建一个Executor,但并没有讨论如何关闭它。Executor的实现通常会创建线程来执行任务。但JVM只有在所有线程全部终止后才会退出,因此,无法正确地关闭Executor,那么JVM将无法结束。
由于Executor以异步方式来执行任务,因此在任何时刻,之前提交任务的状态不是立即可见的。有些任务可能已经完成,有些可能正在运行么,而其他的任务可能在队列中等待执行。当关闭应用程序时,可能采用最平缓的关闭形式(关闭所有已经启动的任务,而且不再接受任何新的任务),也可能采用最粗暴的关闭形式(直接关闭机房的电源),以及各种其他可能的形式。
为了解决执行服务的声明周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法
ExecutorService的生命周期有3种状态:运行、关闭和已终止。ExecutorService在初始创建时处于运行状态。shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成——包括那些还未开始执行的任务。shutdownNow方法将之心粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。在ExecutorService关闭后提交的任务将由“拒绝执行处理器”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务都完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已经终止。通常在调用awaitTermination之后会立即调用shutdown,从而产生同步地关闭ExecutorService的效果。

5)延迟任务与周期任务
Timer类负责管理延迟任务以及周期任务。然而,Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它。可以通过ScheduledThreadPoolExecutor的构造函数或newScheduledThreadPool工厂方法来创建该类的对象。
Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。例如某个周期TimerTask需要每10ms执行一次,而另一个TimerTask需要执行40ms,那么这个周期任务或者在40ms认为执行后快速连续地调用4次,或者彻底“丢失”4次调用(取决于它是基于固定速率调度还是基于固定延时来调度)。线程池能弥补这个缺陷,它可以提供多个线程来执行延时任务和周期任务。
Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不会捕获异常,因此当TimerTask抛出未检查的异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误地认为整个Timer都被取消了。因此,已经没调度但尚未执行的TimerTask将不会再执行,新的任务也不能被调度(这个问题称之为“线程泄露”)
如果要构建自己的调度服务,那么可以使用DelayQueue,它实现了BlockingQueue,并为ScheduledThreadPoolExecutor提供调度功能。DelayQueue管理者一组Delayed对象。每个Delayed对象都有一个相应的延迟时间:在DelayQueue中,只有某个元素逾期后,才能从DelayQueue中执行take操作。从DelayQueue中返回的对象将根据它们的延迟时间进行排序
package chapter6;


import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * @author zhen
 * @Date 2018/11/16 9:27
 * 在 1 小时内每 10 秒钟蜂鸣一次
 */
public class BeeperControl {
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    public static void main(String[] args) {
        BeeperControl bc = new BeeperControl();
        bc.beepForAnHour();
    }

    public void beepForAnHour() {
        final Runnable beeper = new Runnable() {
            public void run() {
                System.out.println("beep");
            }
        };
        final ScheduledFuture<?> beeperHandle = scheduler.scheduleAtFixedRate(beeper, 10, 10, TimeUnit.SECONDS);
        scheduler.schedule(new Runnable() {
            public void run() {
                beeperHandle.cancel(true);
            }
        }, 60*60, TimeUnit.SECONDS);
    }
}
ScheduledExecutorService 的例子
package chapter6;

import java.util.Timer;
import java.util.TimerTask;

import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * @author zhen
 * @Date 2018/11/15 10:03
 * 一个任务抛出异常会终止线程
 */
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 {
        @Override
        public void run() {
            throw new RuntimeException();
        }
    }
}
Timer引发bug的例子

 

3、找出可利用的并行性

Executor框架帮助指定执行策略,但如果要使用Executor,必须将任务表述为一个Runnable。在大多数服务器应用程序中都存在一个明显的任务边界:单个客户请求
1)携带结果的任务Callable与Future
Executor框架使用Runnable作为其基本的任务表示形式。Runnable是一种有很大局限的抽象,虽然run能写入到日志文件或者将结果放入某个共享的数据结构,但它不能反悔一个值或者抛出一个受检查的异常。
许多任务实际上都是存在延迟的计算——执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务,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来获得被封装的初始异常
public interface Callable<V> {
V call() throws Exception;
}

public interface Future<V> {
boolean cancel(boolean mayInterruptUfRunning);
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实例化一个FuntureTask。(由于FutureTask实现了Runnable,因此可以将它提交给Executor来执行或者直接调用它的run方法)
2)在异构任务并行化中存在的局限
3)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中。
4)为任务设置时限
有时候,如果某个任务无法再指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。在优先时间内执行任务的主要困难在于,要确保得到答案的时间不会超过限定的时间,或者在限定的时间内无法获得答案。在致辞时间限制的Future.get中支持这种需求,当结果可用时,它将立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。
在使用限时任务时需要注意,当这些任务超时候应该立即停止,从而避免为计算一个不再使用的结果而浪费计算资源。
package chapter6;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * @author zhen
 * @Date 2018/11/15 10:24
 * 使用Future等待图像下载
 */
public class FutureRenderer {
    private final ExecutorService executor = Executors.newFixedThreadPool(2);
    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<>();
                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());
        }
    }

    public RuntimeException launderThrowable(Throwable throwable){
        return new RuntimeException(throwable);
    }

    public void renderText(CharSequence source){}
    public List<ImageInfo> scanForImageInfo(CharSequence source) {
        return null;
    }
    public void renderImage(ImageData imageData) {}
}
使用Future等待图像下载例子
package chapter6;

import java.util.List;
import java.util.concurrent.*;

/**
 * @author zhen
 * @Date 2018/11/15 10:56
 */
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(imageData);
            }
        } catch (InterruptedException e){
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }

    public RuntimeException launderThrowable(Throwable throwable){
        return new RuntimeException(throwable);
    }

    public void renderText(CharSequence source){}
    public List<ImageInfo> scanForImageInfo(CharSequence source) {
        return null;
    }
    public void renderImage(ImageData imageData) {}
}
使用CompletionService的例子

 

一些有关API:

 

 

 

 

 

 

posted @ 2018-11-16 13:06  guodaxia  阅读(139)  评论(0)    收藏  举报