面试|简述下ThreadPoolExecutor线程池创建过程以及参数含义

0.开篇介绍

线程池是并发里面最经常使用的工具类,所以想要晋升为中级Java工程师,学好并发的前提就是学好线程池。这里会介绍关于线程池的一些内容,比如线程池的创建过程和原理,如何理解线程池的各种状态以及之间的转换,理解面试中常说的FixedThreadPool,SingleThreadExecutor,CachedThreadPool,ScheduledThreadPool四种池原理以及使用场景等等,在分析原理的过程中会给出具体的使用实例,尽量能够理解到位。

这是开篇,关于常规线程池ThreadPoolExecutor的创建过程以及各参数含义的介绍。

1.问题引入

Java语言关于线程池的类图:
面试|简述下ThreadPoolExecutor线程池创建过程以及参数含义_第1张图片
ThreadPoolExecutor就是我们口中常说的线程池,ScheduledThreadPoolExecutor是继承自它的子类,江湖上称周期线程池,它的作用跟它的名字一样,可以周期性的执行某一个任务。这里我们重点学习ThreadPoolExecutor类,整个过程我们思考以下几个问题:

  • 线程池如何创建?
  • 线程池如何接收并执行一个或者多个任务?
  • 线程池中的线程如何创建?何时创建?存活到何时?
  • 线程池中的线程间是如何调度的?即调度机制是什么?
  • 线程池如何存放多余任务?
  • 线程池如何销毁?何时销毁?

2.ThreadPoolExecutor的4种构造函数以及7个参数介绍

在Java世界里,万物皆对象,所以线程池也抽象成一个ThreadPoolExecutor类,每需要一个线程池就可以new一个ThreadPoolExecutor对象;所以创建线程池也是通过new来实现的。JDK通过重载构造函数提供四种不同形式的构造方法:

// 构造函数A  5个参数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
// 构造函数B  6个参数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
// 构造函数C  6个参数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
// 构造函数D  7个参数
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;
}

理解构造函数中每个参数的含义是理解线程池的关键,又因为构造函数A、B和C内部都会调用构造函数D,所以我们首先理解构造函数D中的参数。

  • corePoolSize:核心线程数;是指线程池中需要保留的线程数,即使线程处于空闲状态;但设置allowCoreThreadTimeOut属性(true)外;此时池内的线程经常被称为核心线程;
  • maximumPoolSize:最大线程数;允许线程池中保留的最大线程数,满负荷运行状态下的线程数;
  • KeepAliveTime:线程保持活跃时间;当线程池内线程数大于核心线程数(corePoolSize)时,多余的空闲线程在终止之前等待新任务的最长时间,即空闲线程空闲keepAliveTime时间,若还没有任务则线程销毁,否则执行任务。
  • unit:keepAliveTime参数的单位;其TimeUnit类型是一个枚举类,有七种取值:DAYS,HOURS,MINUTS,SECONDS,MILLISECONDES和MICROSECONDES;根据实际情况取值,一般MINUTS和SECONDS使用较多;
  • workQueue:用于在任务执行前保存任务的队列,这个队列只包含execute方法提交的Runnable任务。常见的队列有三种:直接传递(direct handoffs),无界队列(unbounded queues)和有界队列(bounded queues)。后面会讲到每种队列的具体使用场景。
  • threadFactory:线程工厂;执行器用它创建新的线程;JDK有一个默认的线程工厂:Executors#DefaultThreadFactory,也可以自己实现ThreadFactory接口自定义线程工厂来定义线程的名称、组别或者优先级等。
  • handler:拒绝处理策略;处理因达到线程数量边界和队列容量而导致执行器阻塞时任务被拒绝的情况;一般有四种策略:AbortPolicy,CallerRunsPolicy,DiscardPolicy和DiscardOldestPolicy;当然也可以实现RejectedExecutionHandler接口来自定义拒绝策略。

以上是创建线程池时需要的7个参数,而回过头来看其他构造函数,你会发现:

  • 构造函数A传递5个构造参数,但是使用默认的线程工厂Executors.defaultThreadFactory和默认的拒绝处理策略defaultHandler;
    默认线程工厂构造函数:这里定义了线程所属的线程组以及线程名称的前缀.

    DefaultThreadFactory() {
       SecurityManager s = System.getSecurityManager();
       group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
       namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
    }
    

    默认拒绝策略defaultHandler:

     private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
    

    该处理策略表明线程池在拒绝任务时直接抛出运行时RejectedExecutionException异常。
    另外其他三种处理策略:

    • ThreadPoolExecutor.CallerRunsPolicy:调用execute本身的线程处理这个任务;这提供了一个简单的反馈控制机制,可以降低新任务提交的速度。
    • ThreadPoolExecutor.DiscardPolicy:无法执行的任务被简单地删除。
    • ThreadPoolExecutor.DiscardOldestPolicy:如果执行器没有关闭,则删除工作队列头部的任务,然后重试执行(可能再次失败,导致重复执行)。
  • 构造函数B传递6个构造参数,相比构造函数A只是使用了自定义的线程工厂;

  • 构造函数C传递6个构造参数,它使用默认的线程工厂,其他7个参数都由外部自定义传递。

在构造函数D中除了7个构造参数外,还会初始化AccessControlContext对象的acc的值。acc对象是指当前调用上下文的快照,其中包括当前线程继承的AccessControlContext和任何有限的特权范围,使得可以在稍后的某个时间点(可能在另一个线程中)检查此上下文。

到这里,线程池创建的过程算是结束了,回过头来看它主要决定了7个对象:

  • 该线程池内包含corePoolSize个核心线程;
  • 该线程池内最多包含maximumPoolSize个线程;
  • 该线程池内超过核心线程数的其他空闲线程存活时间;
  • 该线程池存放等待任务的等待队列对象;
  • 该线程池创建线程的线程工厂;
  • 该线程池拒绝多余任务的策略;
  • 创建该线程池时的上下文快照;

3.问题回顾

明白线程池的创建过程,能回答开始提出的几个问题:

3.1 线程池如何创建?

常规线程池通过提供的四个构造函数new出来,并且根据实际情况即参数个数选择对应的构造函数。

3.2 线程池中的线程如何创建?何时创建?存活到何时?

线程池中的线程根据任务的多少由线程工厂创建。如果当前线程数少于核心线程数(corePoolSize),则来一个任务就新创建一个线程;如果当前线程数大于核心线程数且小于最大线程数,新来的任务会优先放到等待队列,等待队列已满则新创建一个线程,线程池内的最大线程不能大于maximumPoolSize。线程池内的存活时间也跟任务数有关;如果线程一直处于工作状态,那么一直存活;如果有池内线程数大于核心线程数且有空闲线程,那么空闲线程等待keepAliveTime之后进行销毁;如果池内线程数小于核心线程数且有空闲线程,线程不会销毁,除非设置了allowCoreThreadTimeOut属性(true/false),其值默认为false,核心线程不会销毁,若设置为true,则空闲核心线程维持keepAliveTime时间后销毁。

3.3 线程池如何存放多余任务?

判定是不是多余任务,就看池内现有线程数是否等于核心线程数,对于核心线程数处理不及时的任务就是多余任务;池通过等待队列把多余任务存起来,等待队列分为三种:

  • Direct handoffs.等待队列(workQueue)的一个很好的默认选择是SynchronousQueue,它将任务传递给线程,而不需要占用它们。在这里,如果没有立即可用的线程来运行任务,则对任务进行排队的尝试将失败,因此将构造一个新线程。此策略在处理可能具有内部依赖项的请求集时避免锁定。Direct handoffs 通常需要无界的最大池大小,以避免拒绝新提交的任务。那么,当任务到达的平均速度继续快于它们能够被处理的速度时,可能会出现无限制的线程增长。
  • Unbounded queues.使用无界队列(例如没有预定义容量的LinkedBlockingQueue)将导致新任务在所有核心大小的线程都忙时在队列中等待。因此,不会创建超过corePoolSize的线程。(因此,maximumPoolSize的值没有任何影响。)这可能适用于当每个任务完全独立于其他任务,因为任务不会影响其他任务的执行;例如,在web页面服务器中。尽管这种类型的排队在消除瞬时请求暴增方面很有用,但当任务平均到达速度超过处理速度时,等待队列可能会无限增长。
  • Bounded queues.当使用有限的maximumPoolSize时,有界队列(例如ArrayBlockingQueue)有助于防止资源耗尽,但调优和控制可能更困难。队列大小和最大池大小可以相互交换:使用大队列和小池可以最小化CPU使用、OS资源和上下文切换开销,但是可能会导致人为的低吞吐量。如果任务经常阻塞(例如,如果它们是I/O绑定的),系统可能能够为比其他方法允许的更多的线程安排时间。使用小队列通常需要更大的池大小,这会使cpu更忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。

4.遗留问题

带着剩下的几个问题继续往下看。

  • 线程池如何接收并执行一个或者多个任务?
  • 线程池中的线程间是如何调度的?即调度机制是什么?
  • 线程池如何销毁?何时销毁?

这几个问题会在下一篇继续分析。

你可能感兴趣的:(Java基础面试题,Java基础知识)