「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,全程无重启。
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,并让动态调优面板直接读这些数据。参数调整从「拍脑袋 + 重启」变成了「看板 + 一行配置」,这也是线程池治理从能用到好用的分水岭。