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 拒绝策略:优雅降级比硬撑更重要

当线程池满了,队列也满了,新任务怎么办?

四种策略,我一个个说:

  1. AbortPolicy:直接抛异常。适合核心交易,不能丢也不能等。
  2. CallerRunsPolicy:谁提交谁执行。我常用这个做背压,让上游感知到压力。
  3. DiscardPolicy:静默丢弃。适合日志、统计等非关键任务。
  4. 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密集? 核心=CPU核数+1 队列:SynchronousQueue 核心=CPU核数×2 队列:ArrayBlockingQueue 队列满? 拒绝策略? CallerRunsPolicy 推荐交易场景 AbortPolicy 严格场景

这张图的核心逻辑是:先判断业务类型,再选队列,最后定拒绝策略。每一步都有对应的最佳实践。

总结一下我的调优口诀

CPU密集核加一,IO密集乘个二。
队列千万别无界,ArrayBlock最稳妥。
拒绝策略看业务,交易场景CallerRun。
动态监控不能少,线上调优才靠谱。

嗯,线程池调优就这些。记住一点:没有万能配置,只有适合你业务的配置。多压测,多观察,慢慢就能找到那个「黄金配比」。


公众号:蓝海资料掘金营,微信deep3321