• 线程池ThreadPoolExecutor简介


    1 前言

    线程池是并发编程中一个重要的概念和技术。大多数异步或并发执行任务都会用到线程池。 线程池,正如其名,它是有一定数量的线程的池子,它会执行被提交过来的任务,执行完一个任务后不会马上结束,它们会继续等待或执行新的任务。线程池有两个重要的概念一个是任务队列,另一个是工作者线程 。任务队列是存放任务的容器,工作者线程会依次不断地到队列中获取任务并执行。

    线程池有这些优点:

    • ① 减少系统资源的消耗。它通过对线程的重用,避免不断创建新线程导致的系统开销。任务过多时,通过排队避免创建过多的线程,减少系统资源的消耗与竞争,确保任务有序完成。

    • ②提高响应速度。当任务到达时,任务无需等待线程的创建完成,它得利用已有的线程立即执行任务。

    • ③提高线程的可控性。线程是稀缺资源,不能无限制地创建,线程池它对线程能统一分配、调度和销毁。

    线程池直接继承于抽象类AbstractExecutorServiceAbstractExecutorService是对ExecutorSerivice接口的默认实现,而ExecutorService又扩展了Executor接口。 

    2 处理任务的流程

    线程池对任务的处理有它自己定义的流程,它对任务的处理流程如下:

    ①线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务(线程池在开始阶段会尽快让池中的线程数达到设定的核心线程数)。如果核心线程池里的线程都在执行任务,则进入下个流程。

    ②线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里(尽量先往阻塞队列中放)。如果工作队列满了,则进入下个流程。

    ③线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务(工作队列放不下了,只有创建新线程来执行任务)。如果已经满了,则交给饱和策略来处理这个任务。

     其基本理念是:①在线程池启动阶段,尽快让池中的线程数达到设定的核心线程数,这里主要从能利用已有线程立即执行之后提交的新任务、避免创建线程而等待的角度考虑;② 在核心线程池满了之后,尽可能向阻塞队列中放入任务,这里是从减少资源消耗的角度考虑,毕竟线程是稀缺资源、不能无限制地创建;③在阻塞队列已满的情况下,已经无法再往队列中放入任务了,此时只能创建新的线程去执行任务,虽然创建线程会消耗系统资源,但是总不能不执行提交的任务;④而在最坏的情况下,线程池中的线程数也达到了设定的最大线程数,此时已无法直接执行任务了,只能按照指定的饱和策略来拒绝任务。

    3 线程池的配置

    ThreadPoolExecutor有4个构造方法,分别需要若干个参数,我们主要通过构造方法参数去配置线程池。我们从其参数个数最多的构造方法看起,其他的构造方法都是直接调用这个构造方法来实现的。

        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.acc = System.getSecurityManager() == null ?
                    null :
                    AccessController.getContext();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }

    这个构造方法有7个参数:

    1) corePoolSize : 核心线程数. 提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。另外还可以调用setCorePoolSize(int)方法来设置核心线程数。

    默认情况下,核心线程不会从预告创建,只有有任务时才创建;核心线程不会因空闲而终止。但以下几个API可以改变这种默认方式。

    int prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

    boolean prestartCoreThread()方法,创建一个核心线程,若所有核心线程已创建则返回false.

    allowCoreThreadTimeOut(boolean)方法,若参数是true,核心线程会因空闲和终止(和其他非核心线程一样,使用keepAliveTime参数作为最大空闲存活时间)。

    2) maximumPoolSize: 线程池可创建的最大线程数。如果队列已满,线程池将创建新的线程执行任务直到达到这个最大线程数。若是使用无界阻塞队列,队列永远也不会满,它不会创建新线程,它会一直往队列中放任务,其结果是一些任务长时间等待、难以被执行。另外还可以调用setMaximumPoolSize(int)方法来设置最大线程数

    3) keepAliveTime:空闲线程存活时间,设置此参数的目的是释放多余的线程资源。它表示线程个数大于corePoolSize时,其他额外空闲线程的存活时间。也就是说,一个非核心线程在空闲等待新任务时,会有一个最长等待时间,若等待时间超过了keepAliveTime,这个线程就会被销毁。若是将此参数设为0,那么所有的线程将一直不会被销毁。若任务较多且任务执行时间较短,可适当增大此参数,提高线程的利用率,避免反复创建新线程。 另外调用setKeepAliveTime(long , TimeUnit )方法也可设置空闲线程存活时间。

    4) unit :参数keepAliveTime的时间单位,可以是“DAYS”(天) 、”HOURS“(时)、“MINUTES”(分)、“SECONDS”(秒)、”MILLISECONDS“(毫秒)、”NANOSECONDS“(纳秒)等

    5) workQueue:工作队列, 用于保存等待执行的任务的阻塞队列。之前的文章并发编程中的阻塞队列概述有对阻塞队列做过介绍,这里只对进行SynchronousQueue特别说明。SynchronousQueue不存储元素的阻塞队列,当尝试排队时,只有正好有空闲线程正在等待接受任务时才会入队成功,否则总是创建新线程执行任务,直到线程数达到maximumPoolSize ,其吞吐量通常要高于LinkedBlockingQueue

    6) threadFactory :线程工厂,主要用于为创建出来的线程设置优先级、取个有意义的名字、是否守护线程等。另外还可调用setThreadFactory(ThreadFactory) 方法设置线程工厂。ThreadFactory是一个接口,它的定义是:

    public interface ThreadFactory {
        Thread newThread(Runnable r);
    }

    ThreadPoolExecutor的默认实现是Executors工具类的静态内部DefaultThreadFactory,这个线程工厂主要是创建一个线程,设置一个名称,设置daemon为false,将优先级设为标准默认优先级,线程名称的格式:”pool-线程池编号-thread-线程编号“。

    7) handler:拒绝策略. 当线程池和队列都满了时,表示线程池已经饱和,此时应采取一些特殊的手段来处理这个新任务。反过来说,拒绝策略只有在队列有界且maximumPoolSize有限大时才会被触发。若队列无界,任务一直往队列中放置,任务一直处于排队中,难以得到执行。若队列有有界、maximumPoolSize无限大,则会创建大量的线程,占满CPU和内存,可能导致程序或系统崩溃。

    默认情况下线程池会使用AbortPolicy策略,此策略会直接抛出异常。线程池内置有4种拒绝策略,这4种拒绝策略都是ThreadPoolExecutor的静态内部类。

    • CallerRunsPolicy, 使用任务提交者的所在线程执行任务;

    • AbortPolicy,直接抛出异常,这是默认的拒绝策略;

    • DiscardPolicy, 不执行任务,将任务丢弃;

    • DiscardOldestPolicy,丢弃队列中最近的任务,然后执行当前任务。

    以上4个类都实现了RejectedExecutionHandler接口,当线程无法接受新任务时,调用拒绝策略的rejectedExecution方法进行相应处理。

    public interface RejectedExecutionHandler {
       void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
    }

    拒绝策略除了在构造方法中指定外,还可调用线程池的setRejectedExecutionHandler方法进行设置。

     

    4 提交任务

    1)线程池有两组提交单任务的方法

    execute(Runnable)用于提交不需要结果的任务,因此无法确定任务是否完成。 而submit系列方法, submit(Runnable, T) submit(Callable<T>)submit(Runnable)都用于提交需要结果的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成(使用“submit(Runnable)”提交任务,get()方法最终返回null),而使用get(long,TimeUnit)方法则会阻塞当前线程一段时间后立即返回,此时返回可能任务未完成。

     

    2)线程池有两组批量提交任务组的方法 invokeAll(Collection)方法用于批量提交任务、等待所有任务完成成后,返回Future的List集合 。invokeAll(Collection, long, TimeUnit)方法是超时版本的 invokeAll(Collection),需要指定超时时间,若超时后还有任务还未完成,这些未完成的任务就会被取消。

    invokeAny(Collection)也用于批量提交任务,但只要有一个任务正常完成(没抛出异常)后,它就返回此任务的结果;在正常返回或异常抛出返回后,其他任务则会被取消(最多只有一个任务能正常执行完成)。invokeAny(Collection, long , TimeUnit )是超时版本的invokeAny(Collection),它对任务的执行耗时做了限制,如果在限定时间内有一任务正常(没抛出异常)完成,就返回此任务的结果 ,其他将任务会被取消;如果没有任务能在限时内成功完成返回,就抛出 TimeoutException; 没有任务正常成功返回(可能是因发生某种异常而返回),将抛出ExecutionException

     

    5 关闭线程池

    shutdown()shutdownNow()方法都能关闭线程池,它们的处理逻辑是:遍历线程池中的工作者线程,然后逐个调用线程的interrupt方法来中断线程,若某些任务不能响应中断,那么它们就无法终止。但两者在细节上有一些区别,shutdownNow()首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown()只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

    只要调用了这两个关闭方法中的任意一个,isShutdown()方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminated()方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown()方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow()方法。

     

    6 合理配置线程池

    要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。

    • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。

    • 任务的优先级:高、中和低。

    • 任务的执行时间:长、中和短。

    • 任务的依赖性:是否依赖其他系统资源,如数据库连接

    性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu +1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu 。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

    优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行(如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行)

    执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU

    尽可能使用有界队列 。有界队列能增加系统的稳定性和预警能力,如果当时我们设置成无界队列,队列永不可能满,那么线程池的队列就会越来越多,有可能会导致内存溢出、程序崩溃。

     

    7 状态监控

    为了监控线程池,我们可以使用一些方法获取线程池的状态信息。

    getTaskCount(): 计划要执行的任务总数

    getCompletedTaskCount(): 线程池已完成的任务数量

    getLargestPoolSize(): 线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。

    getActiveCount() :当前活动(正在执行任务)的工作线程数

    getPoolSize():当前线程池中的工作线程总数

    除此之外,线程池还提供了3个空方法,beforeExecute方法在执行一个任务前被调用,afterExecute方法在一个任务完成后被调用,terminated()方法在线程池停止时被调用。

    我们可继承ThreadPoolExecutor来实现自己的线程池,并以此为基础重写这3个方法来实现自己的监控逻辑。

    protected void beforeExecute(Thread t, Runnable r) { }
    protected void afterExecute(Runnable r, Throwable t) { }
    protected void terminated() { }

    afterExecute方法注释上写了一个这样的使用示例,它能打印导致任务非正常完成的异常信息。

     class ExtendedExecutor extends ThreadPoolExecutor {
       // ...
       protected void afterExecute(Runnable r, Throwable t) {
         super.afterExecute(r, t);
         if (t == null && r instanceof Future<?>) {
           try {
             Object result = ((Future<?>) r).get();
           } catch (CancellationException ce) {
               t = ce;
           } catch (ExecutionException ee) {
               t = ee.getCause();
           } catch (InterruptedException ie) {
               //在捕获中断异常后,中断标志将设为false ,这里调用interrupt恢复中断状态
               Thread.currentThread().interrupt(); // ignore/reset 
           }
         }
         if (t != null)
           System.out.println(t);
       }
     }

    参考:《Java并发编程的艺术》《Java的逻辑》 

     

  • 相关阅读:
    《谈谈推荐系统中的用户行为序列建模》
    《样本权重对逻辑回归评分卡的影响探讨》
    CLOUD计算产品成本嵌套
    冲突操作列表
    查看临时表空间
    设置SQLServer数据库内存
    BPM与OA的区别
    企业门户建设详解
    CRM/PLM/SCM/MES与ERP的联系与区别
    供应链十大优化方法
  • 原文地址:https://www.cnblogs.com/gocode/p/introduction-to-ThreadPoolExecutor.html
Copyright © 2020-2023  润新知