考察点
线程池是最贴近工程实战的并发题,面试官真正想听的是你怎么给生产系统定参数,而不是背 API。关键点:任务进池后的调度顺序(核心线程 → 队列 → 最大线程 → 拒绝)很多人记反;Executors 工厂方法埋的 OOM 雷必须知道。大数据岗位还会结合 Flink/Spark 的线程模型问,或者问业务里异步化改造怎么落地。
参考答案
七个参数和调度流程
ThreadPoolExecutor 的核心参数:
- corePoolSize:核心线程数,常驻不回收(除非开 allowCoreThreadTimeOut)。
- maximumPoolSize:最大线程数。
- keepAliveTime + unit:非核心线程的空闲存活时间,超时被回收。
- workQueue:任务队列。
- threadFactory:线程工厂,生产上必须自定义命名,出问题好排查。
- handler:拒绝策略。
一个任务提交后的走向,这个顺序是高频考点:先创建核心线程执行;核心线程满了,进队列排队;队列满了,创建非核心线程执行(注意不是队列满了才轮到非核心线程出来干活,而是队列满才触发扩容);线程数到 maximumPoolSize 了,走拒绝策略。很多人以为先扩到最大线程再入队,实际相反——队列在扩容之前。这个设计意味着:无界队列会让 maximumPoolSize 形同虚设,因为队列永远满不了。
四种内置拒绝策略
- AbortPolicy(默认):抛 RejectedExecutionException。安全但粗暴,调用方必须处理。
- CallerRunsPolicy:让提交任务的线程自己执行这个任务。天然形成反压——提交方慢了,整个生产速度就降下来,流式系统里很常用。
- DiscardPolicy:静默丢弃,不抛异常。丢了都不知道,生产慎用。
- DiscardOldestPolicy:丢掉队列里最老的任务再重试提交。同样可能丢数据。
生产上还常见自定义策略:写本地磁盘/MQ 落盘兜底、记录监控告警,或者结合信号量在入池前限流。
Executors 工厂的坑
阿里巴巴 Java 开发手册明确禁止用 Executors 快捷方法,原因很具体:
- newFixedThreadPool / newSingleThreadExecutor:队列是无界 LinkedBlockingQueue,任务堆积可能 OOM。
- newCachedThreadPool:队列是 SynchronousQueue(不存任务),maximumPoolSize 是 Integer.MAX_VALUE,突发流量下可能创建无限线程,CPU/内存打爆。
- newScheduledThreadPool:同样是无界队列问题。
正确姿势是自己 new ThreadPoolExecutor,队列选有界的(ArrayBlockingQueue 或带容量的 LinkedBlockingQueue),参数全部显式给出。
线程数怎么定
理论起点是区分任务类型:
- CPU 密集型:线程数 ≈ CPU 核数(或核数 + 1)。多了只会增加上下文切换。Flink 的 TaskManager 默认 slot 数等于 CPU 核数就是这个逻辑。
- IO 密集型:线程大部分时间在等 IO,公式是
核数 × (1 + 平均等待时间 / 平均计算时间)。比如 IO 等待占 90%,大致可以开 核数 × 10。
但公式只是起点,真实系统是压测定出来的。工程做法:按公式给个初始值,压测观察 CPU 利用率、线程池监控指标(活跃线程数、队列深度、任务耗时),逐步逼近。几个原则:IO 密集的核心瓶颈往往在下游(数据库连接、下游接口),线程开太多会把下游打垮,所以线程池大小还要和下游容量匹配;队列别设太大,队列深意味着任务排队久、延迟高,宁可小队列配 CallerRuns 反压;线程池要监控,把 activeCount、queueSize、拒绝次数上报到监控系统,大数据平台里线程池打满引起的隐形积压,不监控很难发现。
补充一点
submit 提交的任务如果抛异常,异常会被封装进 Future,不调用 get() 就悄无声息地吞掉了——生产上要么用 execute 配 UncaughtExceptionHandler,要么 submit 后处理 Future,这个坑栽过的人不少。
可能的追问
- 核心线程满了为什么先入队而不是直接扩线程? 设计初衷是优先复用核心线程、靠队列削峰;队列有界时才能逼出非核心线程。这也解释了为什么 SynchronousQueue 配 CachedThreadPool 是「来一个任务开一个线程」。
- CallerRunsPolicy 有什么副作用? 提交线程(比如 Tomcat 的 NIO 线程或 Netty IO 线程)被拖去执行任务,会阻塞事件循环。在这些框架里用要格外小心,最好配合独立的业务线程池。
- shutdown 和 shutdownNow 的区别? shutdown 不再收新任务,已提交的执行完;shutdownNow 尝试中断正在执行的任务并返回队列里没执行的任务列表。线程池关闭后 isTerminated 才为 true。
- 怎么动态调整线程池参数? setCorePoolSize/setMaximumPoolSize 运行时可直接调,美团开源的动态线程池(DynamicTp)就是基于这个加配置中心做的;队列容量想动态改得自定义 ResizableLinkedBlockingQueue。