4、线程池调优:核心线程数、队列策略、拒绝策略的黄金配比
线程池这东西,说白了就是「干活的人」和「待办的活」怎么搭配的问题。
我见过太多团队,上来就抄个配置,核心线程设成CPU核数,队列用无界队列。结果呢?系统一上线就崩。嗯,这里面的门道,比你想的要深。
4.1 核心线程数:不是越多越好
先问个问题:你的系统是CPU密集还是IO密集?
这个答案直接决定了核心线程数的起点。
- CPU密集型:核心线程数 = CPU核数 + 1。多出来的1个是为了补偿页缺失等小停顿。我个人习惯用
Runtime.getRuntime().availableProcessors() + 1。 - IO密集型:核心线程数 = CPU核数 × 2。但说实话,这只是个起点。我在项目中遇到过,实际IO等待时间占比不同,这个系数要调。
我的经验公式:
核心线程数 = CPU核数 × (1 + 等待时间 / 计算时间)
举个例子:如果一次交易请求,计算耗时5ms,等待数据库返回15ms,那系数就是1 + 15/5 = 4。8核机器,核心线程数就是32。
💡 小技巧:用JMH压测,观察CPU利用率。如果CPU一直跑在90%以上,说明线程数够了。如果CPU只有30%,那线程数可能偏少。
4.2 队列策略:选错了就是灾难
队列是缓冲,也是瓶颈。我见过最惨的一次,就是用了无界队列。
当时一个同事觉得「反正队列大一点,总比丢任务好」。结果流量高峰一来,队列里积压了上百万个任务。内存爆了,GC把整个系统拖垮了。嗯,这就是典型的「好心办坏事」。
三种常用队列,我分别说说:
| 队列类型 | 特点 | 适用场景 | 我的建议 |
|---|---|---|---|
| SynchronousQueue | 不存储任务,直接转交 | 高并发、低延迟交易 | 配合最大线程数使用,适合CPU密集 |
| LinkedBlockingQueue | 可设置容量,FIFO | IO密集型、任务均匀 | 容量别超过1000,否则GC压力大 |
| ArrayBlockingQueue | 固定容量,性能略高 | 内存敏感场景 | 我偏好这个,因为内存可控 |
⚠️ 警告:永远不要用无界队列!
我曾经接手过一个系统,队列里积压了500万条订单。恢复数据花了整整两天。从那以后,我所有项目的队列容量都设了上限。
4.3 拒绝策略:优雅降级比硬撑更重要
当线程池满了,队列也满了,新任务怎么办?
四种策略,我一个个说:
- AbortPolicy:直接抛异常。适合核心交易,不能丢也不能等。
- CallerRunsPolicy:谁提交谁执行。我常用这个做背压,让上游感知到压力。
- DiscardPolicy:静默丢弃。适合日志、统计等非关键任务。
- DiscardOldestPolicy:丢弃最旧的任务。适合实时性要求高的场景。
我的黄金配比(交易引擎场景):
- 核心线程:CPU核数 × 2(IO密集型)
- 最大线程:核心线程 × 2
- 队列:ArrayBlockingQueue,容量512
- 拒绝策略:CallerRunsPolicy
为什么选CallerRunsPolicy?因为交易系统不能丢单。让提交线程自己执行,既保证了不丢,又自然做了限流。
4.4 动态调优:别让配置死在那里
线程池参数不是设完就不管了。我习惯在运行时监控几个指标:
- 活跃线程数 / 核心线程数
- 队列积压量
- 拒绝次数
- 任务平均执行时间
如果发现队列经常积压,但拒绝次数为0,说明核心线程数偏少。如果拒绝次数突然增加,说明流量超预期了。
💡 我常用的监控代码片段:
ThreadPoolExecutor executor = (ThreadPoolExecutor) service;
int activeCount = executor.getActiveCount();
int queueSize = executor.getQueue().size();
long completedCount = executor.getCompletedTaskCount();
// 如果活跃线程接近最大线程,且队列在增长
if (activeCount >= executor.getMaximumPoolSize() * 0.8
&& queueSize > 100) {
// 触发告警,或者动态扩容
log.warn("线程池压力大,活跃:{},队列:{}", activeCount, queueSize);
}
4.5 避坑指南
我踩过的坑,列出来给你参考:
- 坑1:核心线程数和最大线程数设成一样。这样队列满了也不会创建新线程,浪费了弹性能力。
- 坑2:用了无界队列,还设了最大线程数。实际上最大线程数永远不会生效,因为队列永远不会满。
- 坑3:线程池里的线程没有命名。出问题的时候,jstack一看全是「pool-1-thread-1」,根本不知道是哪个业务。
⚠️ 我曾经犯过的错:
有一次线上事故,就是因为线程池参数没调好。核心线程设了4,最大线程设了8,队列用了LinkedBlockingQueue无界。结果流量上来,队列里积压了20万任务,内存涨到4G,GC把CPU吃光了。恢复后我改成了ArrayBlockingQueue容量512,核心线程16,最大线程32,再也没出过问题。
4.6 核心逻辑图
下面这张图,是我总结的线程池调优决策流程:
这张图的核心逻辑是:先判断业务类型,再选队列,最后定拒绝策略。每一步都有对应的最佳实践。
总结一下我的调优口诀:
CPU密集核加一,IO密集乘个二。
队列千万别无界,ArrayBlock最稳妥。
拒绝策略看业务,交易场景CallerRun。
动态监控不能少,线上调优才靠谱。
嗯,线程池调优就这些。记住一点:没有万能配置,只有适合你业务的配置。多压测,多观察,慢慢就能找到那个「黄金配比」。
公众号:蓝海资料掘金营,微信deep3321