微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB 框架、分布式中间件、服务治理等等。
老司机倾囊相授,带你一路进阶,来不及解释了快上车!
多线程可以说是面试官最喜欢拿来问的题目之一了,可谓是老生之常谈,不管你是新手还是老司机,我相信你一定会在面试过程中遇到过有关多线程的一些问题。那我现在就充当一次面试官,我来问你:
现有一个线程池,参数 corePoolSize = 5,maximumPoolSize = 10,BlockingQueue 阻塞队列长度为 5,此时有 4 个任务同时进来,问:线程池会创建几条线程?
如果 4 个任务还没处理完,这时又同时进来 2 个任务,问:线程池又会创建几条线程还是不会创建?
如果前面 6 个任务还是没有处理完,这时又同时进来 5 个任务,问:线程池又会创建几条线程还是不会创建?
如果你此时一脸懵逼,请不要慌,问题不大。
创建线程池的构造方法的参数都有哪些?
要回答这个问题,我们需要从创建线程池的参数去找答案:
java.util.concurrent.ThreadPoolExecutor#ThreadPoolExecutor:
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 个参数,从源码可知,corePoolSize 和 maximumPoolSize 都不能小于 0,且核心线程数不能大于最大线程数。
下面我来解释一下这 7 个参数的用途:
corePoolSize
线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态。如果线程池中的线程少于此数目,则在执行任务时创建。
maximumPoolSize
池允许最大的线程数,当线程数量达到 corePoolSize,且 workQueue 队列塞满任务了之后,继续创建线程。
keepAliveTime
超过 corePoolSize 之后的“临时线程”的存活时间。
unit
keepAliveTime 的单位。
workQueue
当前线程数超过 corePoolSize 时,新的任务会处在等待状态,并存在 workQueue 中,BlockingQueue 是一个先进先出的阻塞式队列实现,底层实现会涉及 Java 并发的 AQS 机制,有关于 AQS 的相关知识,我会单独写一篇,敬请期待。
threadFactory
创建线程的工厂类,通常我们会自顶一个 threadFactory 设置线程的名称,这样我们就可以知道线程是由哪个工厂类创建的,可以快速定位。
handler
线程池执行拒绝策略,当线数量达到 maximumPoolSize 大小,并且 workQueue 也已经塞满了任务的情况下,线程池会调用 handler 拒绝策略来处理请求。
系统默认的拒绝策略有以下几种:
- AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
- DiscardPolicy:直接抛弃不处理。
- DiscardOldestPolicy:丢弃队列中最老的任务。
- CallerRunsPolicy:将任务分配给当前执行 execute 方法线程来处理。
我们还可以自定义拒绝策略,只需要实现 RejectedExecutionHandler 接口即可,友好的拒绝策略实现有如下:
- 将数据保存到数据,待系统空闲时再进行处理
- 将数据用日志进行记录,后由人工处理
现在我们回到刚开始的问题就很好回答了:
线程池 corePoolSize=5,线程初始化时不会自动创建线程,所以当有 4 个任务同时进来时,执行 execute 方法会新建【4】条线程来执行任务;
前面的 4 个任务都没完成,现在又进来 2 个队列,会新建【1】条线程来执行任务,这时 poolSize=corePoolSize,还剩下 1 个任务,线程池会将剩下这个任务塞进阻塞队列中,等待空闲线程执行;
如果前面 6 个任务还是没有处理完,这时又同时进来了 5 个任务,此时还没有空闲线程来执行新来的任务,所以线程池继续将这 5 个任务塞进阻塞队列,但发现阻塞队列已经满了,核心线程也用完了,还剩下 1 个任务不知道如何是好,于是线程池只能创建【1】条“临时”线程来执行这个任务了;
这里创建的线程用“临时”来描述还是因为它们不会长期存在于线程池,它们的存活时间为 keepAliveTime,此后线程池会维持最少 corePoolSize 数量的线程。
为什么不建议使用 Executors 创建线程池?
JDK 为我们提供了 Executors 线程池工具类,里面有默认的线程池创建策略,大概有以下几种:
- FixedThreadPool:线程池线程数量固定,即 corePoolSize 和 maximumPoolSize 数量一样。
- SingleThreadPool:单个线程的线程池。
- CachedThreadPool:初始核心线程数量为 0,最大线程数量为 Integer.MAX_VALUE,线程空闲时存活时间为 60 秒,并且它的阻塞队列为 SynchronousQueue,它的初始长度为 0,这会导致任务每次进来都会创建线程来执行,在线程空闲时,存活时间到了又会释放线程资源。
- ScheduledThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于 Timer。
用 Executors 工具类虽然很方便,我依然不推荐大家使用以上默认的线程池创建策略,阿里巴巴开发手册也是强制不允许使用 Executors 来创建线程池,我们从 JDK 源码中寻找一波答案:
java.util.concurrent.Executors:
// FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// SingleThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// CachedThreadPool
public static ExecutorService newCachedThreadPool() {
// 允许创建线程数为 Integer.MAX_VALUE
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
// 允许创建线程数为 Integer.MAX_VALUE
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public LinkedBlockingQueue() {
// 允许队列长度最大为 Integer.MAX_VALUE
this(Integer.MAX_VALUE);
}
从 JDK 源码可看出,Executors 工具类无非是把一些特定参数进行了封装,并提供一些方法供我们调用而已,我们并不能灵活地填写参数, 策略过于简单,不够友好 。
CachedThreadPool 和 ScheduledThreadPool 最大线程数为 Integer.MAX_VALUE,如果线程无限地创建,会造成 OOM 异常。
LinkedBlockingQueue 基于链表的 FIFO 队列,是无界的,默认大小是 Integer.MAX_VALUE,因此 FixedThreadPool 和 SingleThreadPool 的阻塞队列长度为 Integer.MAX_VALUE,如果此时队列被无限地堆积任务,会造成 OOM 异常。