精选·Java与并发

线程池核心参数、拒绝策略与线程数怎么定

91学AI·2026/7/13·9 阅读

考察点

线程池是最贴近工程实战的并发题,面试官真正想听的是你怎么给生产系统定参数,而不是背 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。

评论 (0)

暂无评论,快来抢沙发吧!

91学AI

© 2026 91学AI · 按岗位学 AI 与大数据. All rights reserved.