← 返回题目列表

线程池的核心参数有哪些?任务提交后的执行流程是怎样的?

高频 中等 第 5 / 31 题 更新于 2026/07/25
线程池ThreadPoolExecutor

简化版

ThreadPoolExecutor 有 7 个参数,核心是:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。提交任务时按固定顺序处理:核心线程 → 排队进队列 → 开非核心线程 → 触发拒绝策略

详细版

七个参数corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime + unit(非核心线程空闲存活时间)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。

提交一个任务的执行流程

  1. 当前线程数 < 核心数 → 直接新建核心线程执行(哪怕有空闲线程也先建,直到核心数满);
  2. 核心线程已满 → 任务进工作队列排队;
  3. 队列也满了,且线程数 < 最大数 → 新建非核心线程执行;
  4. 线程数已达最大数且队列满 → 触发拒绝策略

四种内置拒绝策略

  • AbortPolicy(默认):抛 RejectedExecutionException
  • CallerRunsPolicy:让提交任务的线程自己执行该任务(起到降速/背压作用);
  • DiscardPolicy:默默丢弃新任务;
  • DiscardOldestPolicy:丢掉队列里最老的任务,再尝试提交当前任务。

完整版教学

一、这个执行顺序为什么反直觉

最容易记错的是第 2、3 步的顺序——很多人以为是「核心满了就开非核心线程」,其实是「核心满了先排队,队列满了才开非核心线程」。

为什么这么设计?因为创建线程是昂贵的(要分配栈内存、涉及系统调用)。线程池的哲学是:能复用就复用,能排队就排队,实在扛不住了才多开线程。所以队列是「新开线程」前的缓冲带。

理解这个顺序,才能解释下面那个最经典的坑。

二、无界队列会让 maximumPoolSize 失效

Executors.newFixedThreadPool() 使用容量为 Integer.MAX_VALUELinkedBlockingQueue。在正常资源边界内,它几乎不会被填满,因此任务通常只会排队,线程数不会超过核心线程数,maximumPoolSize 与拒绝策略也很难发挥保护作用。

如果提交速度长期高于处理速度,队列会持续增长,延迟和内存占用随之上升,最终可能触发 OOM。因此生产系统通常显式创建 ThreadPoolExecutor,让容量、拒绝策略和资源预算都可控:

new ThreadPoolExecutor(
    8, 16, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),          // 有界队列,才能触发扩容和拒绝
    new ThreadPoolExecutor.CallerRunsPolicy() // 明确的拒绝策略
);

Executors.newCachedThreadPool() 是另一个极端:它用 SynchronousQueue(不存任务)+ 最大线程数 Integer.MAX_VALUE,任务来了就开线程,可能瞬间创建海量线程把机器压垮。

三、参数怎么定:CPU 密集 vs IO 密集

线程数不是拍脑袋,要看任务类型:

  • CPU 密集型(加密、计算):线程多了只会增加上下文切换开销,核心数设为 CPU 核数 + 1 左右。
  • IO 密集型(网络、数据库、文件):线程大部分时间在等 IO、不占 CPU,可以开更多,经验公式约 CPU 核数 × (1 + 平均等待时间/计算时间),实际常设到核数的几倍。

记忆点:CPU 密集「宁少勿多」,IO 密集「可以多开」。但真实场景要靠压测调,公式只是起点。

四、拒绝策略怎么选

  • 任务不能丢、需要感知失败 → AbortPolicy(抛异常,让上层处理);
  • 想自动降速、别丢任务 → CallerRunsPolicy(提交者自己跑,天然背压);
  • 任务可丢弃(如实时性要求高的监控上报)→ Discard 系列;
  • 需要自定义(落库、告警、降级)→ 实现 RejectedExecutionHandler

五、队列容量要从流量峰值算出来

假设线程池稳定处理能力为每秒 200 个任务,突发流量为每秒 500 个任务并持续 3 秒,若不考虑扩容,积压量约为:

积压任务 = (到达速率 - 处理速率) × 持续时间
         = (500 - 200) × 3 = 900

队列设为 1,000 看似能装下,但排在末尾的任务等待时间可能接近 900 / 200 = 4.5 秒,如果接口 SLA 只有 500 ms,这个队列早已失去业务意义。因此容量要同时受内存与最大可接受排队时间约束。

指标作用
到达速率峰值每秒进入多少任务
服务时间单任务占用线程多久
可接受等待SLA 能留给排队多少时间
任务对象大小队列占用的堆内存
下游容量增加线程是否只会压垮依赖

六、线程数公式只是起点

常见估算是 N_threads ≈ N_cpu × U_cpu × (1 + W/C),其中 W 是等待时间、C 是计算时间。例如 8 核、目标 CPU 利用率 80%、W/C=4,初值约为 8×0.8×5=32。它假设任务特征相对稳定,真实值还受上下游限流、锁竞争、GC 和容器 CPU 配额影响。

CPU 密集任务通常从接近有效核心数开始;I/O 密集可多一些,但必须通过压测观察吞吐、P99、CPU、队列长度和拒绝数。线程越多并不等于吞吐越高,上下文切换和共享资源竞争会形成拐点。

记忆钩子:线程池不是“把任务藏进队列”,而是一个背压器;线程、队列、拒绝策略共同决定系统在过载时是等待、降级还是把压力扩散给下游。

七、拒绝策略必须匹配业务语义

AbortPolicy 明确失败,适合让上层感知过载;CallerRunsPolicy 让提交线程执行任务,可形成自然反压,但如果提交线程是关键事件循环就可能拖死入口。DiscardPolicyDiscardOldestPolicy 会丢任务,只能用于业务确认允许丢弃且有监控的场景。

任务提交成功也不等于业务完成。关闭时要区分 shutdown() 的有序停止与 shutdownNow() 的中断请求,并用 awaitTermination 等方式等待或升级处置。

八、常见误区与追问

  • 误区:核心线程满后会立即创建非核心线程。 对有界队列的典型流程是先尝试入队,队列满后才在未到 maximumPoolSize 时扩线程。
  • 误区:maximumPoolSize 在任何队列下都能控制峰值线程数。 无界队列几乎总能接收任务,通常不会走到扩展非核心线程的分支。
  • 误区:Executors 工厂方法绝对不能使用。 风险在于不了解其无界队列或线程策略;受控场景可用,但服务端应明确容量与过载行为。
  • 追问:为什么队列越大不一定越安全? 大队列会隐藏过载、增加等待和内存占用,任务可能在执行前就已超时。
  • 追问:CallerRunsPolicy 有什么风险? 它在提交线程执行任务,能反压,也可能阻塞事件循环、调度线程或持锁线程。
  • 追问:线程池大小怎样验证? 在生产式负载下同时观察吞吐、延迟分位数、CPU、活跃线程、队列和拒绝次数。
  • 追问:线程池之间为什么要隔离? 慢依赖或高流量业务若共用池,会占满线程和队列,拖累无关任务。

九、加强记忆

七个参数要沿执行路径记:任务先由 corePoolSize 范围内线程承接,核心忙时尝试入队,队列满且线程数小于 maximumPoolSize 才扩容,再饱和才执行拒绝策略;keepAliveTime 管理符合条件的空闲线程,threadFactory 和 handler 决定可观测性与过载动作。线程数从 CPU、等待/计算比得到初值,队列容量则由峰值差额、等待 SLA 和任务内存共同约束。无界队列会让 maximumPoolSize 大多失去作用,大队列还会把过载变成长尾延迟。最终必须用吞吐、P99、队列与拒绝指标压测,而不是照抄固定公式。