这可能是史上最全的Java线程池总结了

515 阅读5分钟

一、背景

Java线程池的写法和参数是面试中出现频率很高的基础题。越是基础的东西,特别是对高阶职位的面试者,需要回答的符合自己面试的职位等级。 这里也不能说是一个多么好的答案,只是说如果是我,我怎么回答,仅供参考。以下回答可想象为面试官的问题是:谈谈线程池。主要的思路是作为一个宽泛的问题,回答需要体现结构化的思维,这是必选项。在此基础上,可以体现深度,这是加分项。

二、回答

1. 线程池设计目标

Java的线程主流实现都是采用内核级线程实现,创建线程要进行操作系统状态切换。为了避免资源过度消耗需要设法重用线程执行多个任务。线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。

2. 线程池实现

如上图所示,线程池的Java实现都是继承于JUC(java.util.concurrent)的Executor接口。这个接口只有一个execute方法,代表了其行为。ExecutorService接口继承Executor,加上了生命周期的处理方法。其常用实现类如:ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool的默认参数构造类都有在Executors这个工具类里直接实例化。但是阿里巴巴开发手册不推荐使用Executors这个工具类。

于是,我们需要自己了解ThreadPoolExecutor初始化参数的含义和使用方法。

/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters and default rejected execution handler.
 *
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 * @param unit the time unit for the {@code keepAliveTime} argument
 * @param workQueue the queue to use for holding tasks before they are
 *        executed.  This queue will hold only the {@code Runnable}
 *        tasks submitted by the {@code execute} method.
 * @param threadFactory the factory to use when the executor
 *        creates a new thread
 * @throws IllegalArgumentException if one of the following holds:<br>
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code threadFactory} is null
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

ThreadPoolExecutor有6个参数,第一个是核心线程数,如果线程池无事可做,还是会保留这些线程。第二个是最大线程数,超过核心线程数的部分都会在第三个和第四个参数合起来决定的最长空闲存活时间超过后被剔除。第五个参数是阻塞队列,线程忙不过来要去这里面排队。最后一个是线程池工厂,主要决定队列也装不下的线程怎么处理,默认策略是抛出异常。

3. 线程池的5种状态设计

4. 线程池的底层原理

Java线程的实现是通过调用native方法调用操作系统的pthread API,由内核线程统一管理。一个实现了Runnable的类只是标识可用多线程运行,真正产生大开销的是new Thread()时进行的内核调用。而线程池技术将worker线程缓存下来进行重用。

三、补充问答

1. 线程池的关闭

关闭线程池可以调用shutdownNow和shutdown两个方法来实现。 shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。 shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

2. 线程池都有哪几种工作队列

  • ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
  • SynchronousQueue 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue 一个具有优先级的无限阻塞队列。

3. ForkJoinPool的原理

本质上将一个任务进一步细分,内部使用“工作窃取”算法,让任务在各个CPU上尽可能均衡。

  • 每个工作线程都有自己的工作队列WorkQueue;
  • 这是一个双端队列,它是线程私有的;
  • ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头,工作线程将以LIFO的顺序来处理工作队列中的任务;
  • 为了最大化地利用CPU,空闲的线程将从其它线程的队列中“窃取”任务来执行;
  • 从工作队列的尾部窃取任务,以减少竞争;
  • 双端队列的操作:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的;
  • 当只剩下最后一个任务时,还是会存在竞争,是通过CAS来实现的;

四、总结

本文的回答部分从线程池实现目标、实现和关键设计、底层原理四个方面说明线程池,具有一定的系统性。补充问答部分作为问题的深入在面试时被深入提问问到或者面试官示意继续深入来说的时候使用。如果觉得本文对你有帮助的话,请你也不要吝啬你的赞,你们的支持是对我最大的鼓励。想要知道更多Java基础知识和面试答案的我这边整理了一个我自己的GitHub仓库:Java小白修炼手册,大家如果有需要可以自行查看