「CPU 核数乘以 2」这类线程池口诀流传很广,但照抄口诀翻车的事故我见过不止一次。参数设计的本质是理解队列、拒绝策略和业务 SLA 之间的联动关系。

核心参数的权衡

先厘清一个关键认知:newFixedThreadPool 用的无界 LinkedBlockingQueue 是生产事故重灾区——任务堆积直到 OOM,而线程数永远不会增长。生产代码必须显式 new ThreadPoolExecutor,逐个参数想清楚。

  • corePoolSize:稳态并发量决定的常驻线程数,IO 密集型可参考 QPS × 平均耗时(秒)估算。
  • maximumPoolSize:突发流量的缓冲上限,配合有界队列使用才有意义。
  • workQueue:有界队列(如容量 1000 的 ArrayBlockingQueue),容量 = 可接受的排队延迟 × QPS。
  • rejectedHandler:按业务语义选择——核心交易用 CallerRunsPolicy 反压上游,可丢弃任务用 DiscardPolicy 并记录日志。

动态调优:参数在线生效

线上流量有波峰波谷,固定参数总有不合适的时候。ThreadPoolExecutor 的 setCorePoolSize / setMaximumPoolSize 支持运行时修改,配合配置中心就能实现参数热调。我们在大促前把推送线程池 core 从 8 调到 32,全程无重启。

java
1@Component2public class DynamicThreadPool {34    private final ThreadPoolExecutor executor = new ThreadPoolExecutor(5            8, 32, 60, TimeUnit.SECONDS,6            new ArrayBlockingQueue<>(1000),7            new ThreadFactoryBuilder().setNameFormat("push-worker-%d").build(),8            new ThreadPoolExecutor.CallerRunsPolicy());910    // 监听配置中心变更,在线调整参数11    @ConfigListener(key = "push.thread-pool")12    public void onConfigChange(PoolConfig config) {13        int newMax = config.getMaxPoolSize();14        int newCore = Math.min(config.getCorePoolSize(), newMax);15        executor.setMaximumPoolSize(newMax);   // 先调大 max,再调 core,避免校验异常16        executor.setCorePoolSize(newCore);17        executor.setKeepAliveTime(config.getKeepAliveSeconds(), TimeUnit.SECONDS);18    }19}
动态调优解决的是「参数可以改」,监控告警解决的是「知道该不该改」——两者缺一不可。

监控:四个必看指标

  • activeCount / maximumPoolSize:线程使用率,持续接近上限说明容量不足。
  • queueSize / queueCapacity:队列水位,超过 80% 告警,是扩容的前置信号。
  • rejectedCount:拒绝次数,出现即代表当前配置已无法承载流量。
  • taskDuration P99:任务真实耗时,是所有容量估算公式的输入。

我们把以上指标接入 Micrometer + Grafana,并让动态调优面板直接读这些数据。参数调整从「拍脑袋 + 重启」变成了「看板 + 一行配置」,这也是线程池治理从能用到好用的分水岭。