天天看点

Thread专题(5) - 任务执行

此文被笔者收录在系列文章 ​​​架构师必备(系列)​​ 中

所谓任务就是抽象、离散的工作单元。把一个应用程序的工作分离到任务中,可以简化程序的管理,这种分离还在不同事务间划分了自然的分界线,在程序出现错误时可以很方便地进行恢复,还有利于提高程序的并发性。

围绕任务执行来管理应用程序时,第一步要指明一个清晰的任务边界,理想情况下,任务是独立活动的,它的工作并不依赖于其他任务的状态、结果或边界效应,独立有利于并发性。应用程序应该在负荷过载时平缓地劣化,而不应该负载一高就简单地以失败告终。为了达到这些目的,要选择一个清晰的任务边界,并配合一个明确的任务执行策略。现在的很多服务器都选择了自然边界,比如单个客户请求。

一、JAVA中的线程调度

抢占式

抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。这是JAVA中的实现方案,相关算法如下:

  • 优先调度算法:先来先服务算法FCFS和短作业优先调度算法,公平机制
  • 高优先权优先算法:非公平,用于非实时应用中
  • 时间片轮询算法:

协同式

协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

显式的为任务创建线程

下面这个例子称为“每任务每线程”,这种方法提高了响应性和更大的吞吐量,但在请求的速度超出服务器的请求处理能力时,就不适合了。在开发原型阶段会表现良好。

public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            //accept是一个阻塞操作,so只有在有连接的时候才会往下执行。
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }};
            new Thread(task).start();
        }}      

“每任务每线程”的程序缺点如下:

  • 线程生命周期的开销,如果请求是频繁且轻量级的,就会消耗大量的计算资源。
  • 资源消耗量,主要是针对内存。如果可运行的线程数多于可用的处理器数,线程将会空闲。大量空闲线程占用更多的内存,给GC带来压力,而且会存在大量线程在竞争CPU资源,还会产生其他的性能开销。如果有足够多的线程保持所有CPU忙碌,那么再创建更多的线程是百害无一利的。
  • 稳定性,应该限制可创建线程的数目,这个数目受OS、JVM启动参数、Thread的构造函数中请求的栈大小等因素影响。如果打破了这些规则可能会收到OutOfMemoryError错误。(在32位的机器上,主要的限制因素是线程栈的地址空间,每个线程都维护着两个执行栈,一个用于java代码,另一个用于原生代码,典型的JVM默认会产生一个组合的栈,大小在0.5M大小左右,可以通过-Xss JVM参数或者通过Thread的构造函数修改这个值。如果为每个线程分配了大小232字节的栈,那么你的线程数量将被限制在几千到几万间不等)。

这种实现方式只能当作练习,不能应用到正式的环境中去。

二、Executor框架

线程是使任务异步执行的机制,作为Executor框架的一部分,它是基于生产—消费模式设计,java.util.concurrent提供了一个灵活的线程池实现。

Executor很简单但可用于异步任务执行,支持不同类型的任务执行策略,还为任务提交和任务执行之间的解耦提供了标准的方法,还提供了对生命周期的支持以及钩子函数等。在生产—消费模式中,提交任务是执行者是生产者,执行任务的线程是消费者。

Executor接口的实现相当于一个模板或抽象模板实现,是把Runnable委托给Executor来执行。所以可以定义自己的Executor实现类。

//创建一定长的线程池,这个配置是一次性的 
  private static final Executor exec =  Executors.newFixedThreadPool(100);
  public void task(){
    while (true){
      Runnable task  = new Runnable(){
        @Override
        public void run() {
          // TODO Auto-generated method stub
        }
      };
      exec.execute(task);
    }//end while
  }      
//每任务每线程的Executor实现
class TaskPerThreadExecutor implements Executor{
  @Override
  public void execute(Runnable command) {
    new Thread(command).start();
  }
}      
//单线程的Executor实现,这时的 TaskSingleThreadExecutor相当于一个委托类
class TaskSingleThreadExecutor implements Executor{
  @Override
  public void execute(Runnable command) {
    command.run();
  }
}      

执行策略

将任务的提交与任务的执行进行解耦,价值在于让你可以简单地为一个类给定的任务制定执行策略,并且保证后续的修改不至于太困难,执行策略指明了:

  • 任务在什么线程中执行;
  • 任务以什么顺序执行(FIFO、LIFO、优先级);
  • 可以有多少个任务并发执行;
  • 可以有多少个任务进入等待执行队列;
  • 如果系统过载,需要放弃哪个任务并且如何通知Application知道这一切;
  • 在一个任务的执行前与后,应该插入什么处理操作。

执行策略是资源管理工具,最佳策略取决于可用的计算资源和你对服务质量的需求,将任务的提交与执行进行分离,有助于在部署阶段选择一个与当前硬件最匹配的执行策略。所以以后的程序中尽量少用或不用new Thread(runnable).start()这种方式而改用Executor委托执行。

线程池

线程池是与工作队列(作用是持有所有等待执行的任务的容器)紧密绑定的,使用线程池可以避免每次创建销毁线程的性能开销,而重用线程池中已有的线程。

Executors中有很多静态工厂方法用来创建一个线程池:

  • newFixedThreadPool:创建一个定长的线程池,直到达到最大长度,如果一个线程由于非预期的Exception而结束,线程池会补充一个新的线程。
  • newCacheThreadPool:创建一个可缓存的线程池,它不会对池的大小有任何限制,如果当前线程池的长度超过了处理的需要时,它可以灵活地回收空闲的线程,当需求增加时,它可以添加新的线程,前两种线程池都有可能会造成内存耗尽。如果池里有未被回收的线程,是可被重用的,回收时间是60秒;
  • newSingleThreadExecutor:创建一个单线程化的executor,它只创建唯一的工作线程来执行任务,如果这个线程异常结束,会有另一个取代它,executor会保证任务依照任务队列的规定执行。但和顺序编程存在很大差别,它还是脱离主线程以单独线程的方式执行,只不过一次执行一个。
  • newScheduledThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。

使用了Executor框架可以有各种机制去调优、管理、监视、记录日志、错误报告以及其他可能的事情,如果不用Executor框架这些事情很难去做。

           如果线程到达的时间大于任务执行的速度,”每任务每线程”的策略还是会由于积压导致内存耗尽,使用一个有限的工作队列,可以很好的解决这个问题。

Executor生命周期

Executor实现通常只是为执行任务而创建线程,JVM会在所有线程全部终止后才通出,因此无法正确关闭Executor,进而会阻塞JVM的结束。

因为Executor是异步地执行任务,所以有很多不确定因素,为了解决执行服务的生命周期问题,提供了ExecutorService接口,它扩展了Executor接口同时添加了一些用于生命周期(运行、关闭、终止)管理的方法。

public interface ExecutorService extends Executor {
    //不在接收新的任务后平缓关闭
    void shutdown();
    //强制停止正在运行和队列中等待的任务,并返回列表以便序列化等操作,下次启动时恢复
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    //转换ExecutorService的状态,同时调用shutdown()方法 
    boolean awaitTermination(long timeout, TimeUnit unit)  throws InterruptedException;
}      
private final ExecutorService exec = Executors.newCachedThreadPool();
    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();
    }
    private void log(String msg, Exception e) {
        Logger.getAnonymousLogger().log(Level.WARNING, msg, e);
    }
    void handleRequest(Socket connection) {}      

一旦所有的任务全部完成后ExecutorService会转入终止状态,可以调用awaitTermination等待ExecutorService到达终止状态,也可以轮询isTerminated判断ExecutorService是否已经终止。通常shutdown会紧随awaitTermination之后,这样可以产生同步关闭ExecutorService的效果。

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

Timer工具管理任务的延迟以及周期执行,但是它有一些问题:1、它只创建唯一的线程来执行所有的timer任务,如果一个timer任务执行很耗时,可能会导致其它TimerTask的时效准确性出问题,不加处理可能会造成任务丢失的情况;2、如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。Timer线程并不捕获异常,这时会造成Timer线程的终止,并且不会自动恢复,会错误的认为整个Timer都被取消了(线程泄露);3、另外Timer的调度是基于系统时间的。

所以现在应该用ScheduledThreadPoolExecutor(ScheduledThreadPoolExecutor只支持相对时间)来取代Timer。并且不在使用Timer类。这得益于DelayQueue。如果需要构建自己的调度服务,可以使用DelayQueue,它是BlockingQueue的实现,为ScheduledThreadPoolExecutor提供了调度功能,DelayQueue管理着一个包含Delayed对象的容器。每个Delayed对象都与一个延迟时间相关联,只有在元素过期后,DelayQueue才让你执行take操作获取元素。从DelayQueue中返回的对象将依据它们所延迟的时间进行排序。以下是有问题的一个例子:Timer的混乱行为:

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

三、寻找可强化的并行性

​对于并行性的任务,要考虑的一个很重要的因素是任务边界问题,尽量做到任务之间的松耦合或解耦。这个问题很难下一个指导性的结论,需要在设计时权衡考虑,下面的几个例子从需求角度设计了几个程序的不同并发性,IE渲染HTML的例子:

顺序的渲染页面元素

​这种方式基本是行处理的方式,页面等待时间可能会很长。效率比较低下。

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

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

​Executor框架使用Runnable做为其任务的基本表达形式,但Runnable的功能很有限,它不能返回一个值或抛出受检查的异常。

  • Callable主要用于计算时间长的任务,比如下载、复杂运算、数据库操作。它会在主进入点call--等待返回值,并为可能抛出的异常预告做好了准备,Executors包含一些工具方法,可以把其他类型的任务封装成一个Callable。Callable<void>===Runnable。

在Executor框架中,如果任务执行要花费很长时间,任务可以手动取消,但对于已经开始的任务,只有中断后才可以取消,取消一个已完成的任务不会有任何影响。

  • Future可以描述任务的生命周期,并提供了相关的方法来获得任务的结果、取消任务以及检验任务是否完成。Future和ExecutorService这些线程相关的对象的生命周期是单向的,无法回退。Future的get方法是个阻塞方法, 注意get处理异常的能力,它会把异常重新包装后再抛出。

ExecutorService的所有submit()方法都返回一个Future。可以将一个Runnable或Callable提交给executor,然后得到一个Future。也可以显示的为Runnable或Callable创建一个FutureTask。FutureTask实现了Runnable接口。所以可以直接交给ExecutorService来执行,也可以直接调用run方法。

在java6中,ExecutorService的所有实现都可以覆写AbstractExecutorService中的newTaskFor方法,以此控制Future的实例化,以及对应的已提交的Runnable和Callable。

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);
//    FutureTask future = new FutureTask(task);
//    executor.submit(future);//这两行代码和上面一行代码是等价的。

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

并行运行类任务的局限性

上例的程序通过两个异步的任务期望获得性能上的提升。如果没有在相似的任务之间发现更好的并行性,那么并行运行方法应有的好处会逐渐减少。更为严重的是,在给多个工作者划分相异任务时,各个任务的大小可能完全不同,比如A任务执行时间是B的10倍,那么整个过程仅仅加速了9%左右。

在多个工作者之间划分任务,总会涉及到一些任务协调上的开销,为了使划分任务是值得的,这一开销不能多于通过并行性带来的生产力的提高。上例中如果下载图像的速度远远小于渲染文本的速度,不如用顺序执行程序,这样还能减小代码的复杂度。所以合理的并行任务需要:

大量相互独立且同类的任务进行并发处理,会将程序的任务量分配到不同的任务中,这样才能真正获得性能的提升。

CompletionService:当Executor遇见BlockingQueue

如果向Executor提交了一个批处理任务,并且希望在它们完成后获得结果,为此可以保存与每个任务相关联的Future,然后不断地调用timeout为0的get,来检验Future是否完成。这样做行的通,但可以用CompletionService(完成服务)代替。

CompletionService整合了Executor和BlockQueue的功能,可以将Callable任务提交给它去执行,然后使用类似于队列中的take和poll方法,在结果完整可用时获得这个结果,像一个打包的Future。ExecutorCompletionService是实现CompletionService接口的一个类,并将计算任务委托给一个Executor。

ExecutorCompletionService在构造函数中创建一个BlockingQueue,用它去保存完成的结果。计算完成时会调用FutureTask中的done方法,当提交一个任务后,首先把这个任务包装为一个QueueFuture,它是FutureTask的一个子类,然后覆写done方法,将结果加入到BlockingQueue中,take和poll方法委托给了BlockingQueue,它会在结果不可用时阻塞。

下面这个例子的性能会比前一个例子的性能高,因为算法不同,这里的实现是每需要下载一个图,就创建一个独立的任务,并在线程池中执行它们,将上面例子中的顺序下载过程转化为并行的,这能减少下载所有图像的总时间。从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());
        }
    }      

为任务设置时限

在预定时间内执行任务的主要挑战:

  • 要确保在得到答案或者发现无法从任务中获得结果的这一过程所花费的时间,不会比预定的时间更长,Future.get的限时版本符合这个条件,它在结果准备好后立即返回,如果在时限内没有准备好,就会抛出TimeoutException。
  • 当它们超时后应该可以停止它们,为了达到这个目的,可以让任务自己管理它的预定时间,超时后就中止执行或取消任务。Future可以完成这样的功能,在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;
}      
import java.util.concurrent.*;

public class TimeBudget {
    private static ExecutorService exec = Executors.newCachedThreadPool();

    public List<TravelQuote> getRankedTravelQuotes(TravelInfo travelInfo,  
                                      Set<TravelCompany> companies,
                                      Comparator<TravelQuote> ranking, 
                                     long time, TimeUnit unit){
        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;
    }

}

class QuoteTask implements Callable<TravelQuote> {
    private final TravelCompany company;
    private final TravelInfo travelInfo;

    public QuoteTask(TravelCompany company, TravelInfo travelInfo) {
        this.company = company;
        this.travelInfo = travelInfo;
    }

    TravelQuote getFailureQuote(Throwable t) {
        return null;
    }

    TravelQuote getTimeoutQuote(CancellationException e) {
        return null;
    }

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

interface TravelCompany {
    TravelQuote solicitQuote(TravelInfo travelInfo) throws Exception;
}

interface TravelQuote {
}

interface TravelInfo {
}