
这期拆解 Java 线程池参数面试的 5 个高频扣分点,从核心线程数、队列、最大线程数、拒绝策略到监控验证,帮你把参数题答成有业务取舍的调优思路。
corePoolSize 是核心线程数,maximumPoolSize 是最大线程数,workQueue 是阻塞队列。面试官真正想听的是:任务来了以后先建线程、先进队列、还是直接拒绝?系统流量变大时,你准备让延迟涨、内存涨,还是线程数涨?ThreadPoolExecutor 的官方文档已经把执行顺序说得很清楚:提交新任务时,如果运行线程数小于 corePoolSize,线程池倾向于新建线程;如果已经达到核心线程数,线程池倾向于把任务放进队列;如果任务无法入队,才会在不超过 maximumPoolSize 的前提下继续创建线程;再放不下就触发拒绝。1我不会先说「核心线程数是多少」。我会先说明任务进入线程池后的路径:核心线程没满先建线程,核心线程满了先进队列,队列满了才扩到最大线程数,再满就走拒绝策略。所以参数之间要一起看,不能单独调一个数字。
maximumPoolSize 什么时候才生效,也知道队列不是一个无关的容器。| 任务类型 | 面试里该怎么说 | 参数取舍 |
|---|---|---|
| CPU 计算 | 主要吃 CPU,线程太多会增加上下文切换 | 核心线程数接近 CPU 并行能力,队列别太长 |
| IO 等待 | 大量时间卡在网络、数据库或第三方接口 | 可以允许更多线程,但必须盯住下游承载能力 |
| 混合任务 | 既有计算,又会等外部系统 | 先压测,再按等待时间、响应时间和错误率调整 |
ThreadPoolExecutor 文档也提醒,较大的队列和较小的线程池会降低 CPU 使用、操作系统资源和上下文切换开销,但可能造成吞吐偏低;较小的队列通常需要更大的线程池,让 CPU 更忙,也可能带来调度开销。1如果是发券、发短信、调用外部接口这类 IO 等待多的任务,我不会只看 CPU 核数。我会同时看接口耗时、下游限流、队列积压和超时率。线程数调大只是让更多请求同时等,不代表系统真的处理得更快。
BlockingQueue 的官方文档说明,阻塞队列可以有容量上限;如果没有内在容量限制,remainingCapacity 会返回 Integer.MAX_VALUE。2 这在面试里对应一个很直接的问题:任务来得比处理快时,你是让请求排队,还是尽早暴露压力?我倾向于用有界队列,因为队列本质上是在买时间。它能削峰,但不能创造处理能力。队列容量要和业务能接受的等待时间一起算,比如单任务平均处理 50ms,允许最多等 2 秒,那队列容量就不能拍脑袋写几万。
maximumPoolSize 不是任务一多就立刻生效。按官方执行顺序,只有当核心线程数已满,并且任务队列也放不进去时,线程池才会尝试创建超过核心线程数的新线程。1maximumPoolSize 写得很大,这个最大线程数大概率只是摆设。Oracle 的 Executors.newFixedThreadPool 文档也说明,固定线程池使用固定数量的线程和共享无界队列;所有线程都忙时,新增任务会在队列中等待。3因为队列先接住了任务。只要队列还能放,线程池不会急着扩到最大线程数。要让最大线程数参与削峰,就不能只配一个无限队列,通常要结合有界队列、业务超时时间和拒绝策略一起设计。
execute 提交的新任务会被拒绝,并交给 RejectedExecutionHandler 处理。JDK 预置了 4 种策略:AbortPolicy 抛出异常,CallerRunsPolicy 让提交任务的线程自己执行,DiscardPolicy 直接丢弃,DiscardOldestPolicy 丢弃队列头部任务后重试。1| 策略 | 可以怎么解释 | 面试官想听的边界 |
|---|---|---|
AbortPolicy | 失败显性化,调用方能拿到异常 | 适合不能静默丢的核心链路 |
CallerRunsPolicy | 提交方自己执行,变相降低提交速度 | 会拖慢调用线程,不能随便放在请求主链路 |
DiscardPolicy | 新任务直接丢弃 | 只适合允许丢的非核心任务,如部分日志或埋点 |
DiscardOldestPolicy | 丢掉最早排队的任务,再尝试提交新任务 | 如果队列有优先级,可能误丢更重要的任务 |
CallerRunsPolicy 可以形成一种简单的限速效果,因为调用线程会被迫执行任务;而 DiscardOldestPolicy 和优先级队列搭配时可能丢掉优先级最高的任务。4如果是支付回调、订单状态同步,我不会用静默丢弃。要么抛异常让上游重试,要么落失败表补偿。如果是可丢的行为日志,可以接受降级丢弃,但要打点统计丢弃量。拒绝策略不是技术偏好,是业务能不能丢的问题。
ThreadPoolExecutor 本身维护了完成任务数等基础统计,并允许通过 getQueue() 访问工作队列做监控和调试;文档也提醒,getQueue() 不建议用于其他目的。1 所以面试里可以说「监控」,但别把生产逻辑建立在直接操作内部队列上。我会先按任务进入线程池的路径解释参数:核心线程没满先建线程,满了先进队列,队列满了才扩到最大线程数,再满就触发拒绝。参数不是单独看的。CPU 密集任务线程数不能太大,IO 等待多的任务可以适当放宽,但要看下游连接池、接口耗时和超时率。队列我一般会用有界队列,因为队列是在买等待时间,不是在提升处理能力。拒绝策略要按业务能不能丢来选,核心链路不能静默丢。最后用活跃线程数、队列长度、任务耗时、拒绝次数和下游状态验证参数是否合理。
docs.oracle.com
docs.oracle.com
docs.oracle.com
baeldung.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.