跳转到内容

线程池与队列

用途:配置线程池参数、排查线程池导致的堆积与 OOM 时查阅

核心参数(ThreadPoolExecutor)

「核心参数(ThreadPoolExecutor)」小节链接
参数 含义 常见错误
corePoolSize 常驻线程数 设太大导致上下文切换与内存占用
maximumPoolSize 最大线程数 与无界队列搭配时永远不生效
keepAliveTime 非核心线程空闲存活 短任务场景可延长,减少反复创建
工作队列 缓冲待执行任务 用 Executors 默认无界队列 → 堆积 OOM
拒绝策略 队列满且线程达上限时 默认 AbortPolicy 抛异常被上层吞掉
线程工厂 命名与守护属性 无名线程无法排障

执行顺序:核心线程 → 入队 → 扩到最大线程 → 拒绝。理解这个顺序才能解释“为什么线程数没涨上来”。

先给初值,再用压测与指标修正:

任务类型 初值经验 依据
CPU 密集 核数 + 1 超过核数只会增加切换
IO 密集(下游快) 核数 × (1 + 等待时间/计算时间) 利特尔法则:并发数 ≈ QPS × 平均耗时
下游慢/不确定 小池 + 有界队列 + 快速拒绝 保护自身与下游

关键约束:线程数不是越大越好。下游只有 200 个连接时,开 800 线程只会把排队从本地搬到下游。

  • 队列必须有界,容量由可接受的等待时间与内存预算决定(容量 ≈ QPS × 可接受排队时长)。
  • 拒绝策略要显式选择并配套监控:CallerRunsPolicy 会反压调用方(可能拖慢上游线程),Abort 抛异常需要上层降级,自定义可落盘重试或转异步。
  • 拒绝次数、队列长度、任务等待时间必须作为指标暴露。

不同 SLA 的任务不要共用一个池:慢任务会占满线程饿死快任务。按下游依赖、业务优先级或租户维度拆分线程池,配合独立监控。

同理,一个进程内线程池数量要可控并命名,否则线程总数失控。

风险 说明 对策
任务异常被吞 submit 的异常只在 Future.get() 时抛出 重写 afterExecute 或用 execute + 捕获
ThreadLocal 污染 线程复用导致上下文串号 任务结束清理(try-finally remove)
嵌套提交死锁 父任务占满线程并等待子任务 拆池或避免同步等待子任务
关闭丢任务 shutdownNow 丢弃队列 优雅关闭:停接收 → 等在途 → 超时后处理剩余
无界重试 拒绝后无限重投 重试预算 + 退避

生产系统建议把核心/最大线程数与队列告警做成运行时可调(自建或用开源组件),因为容量随业务变化,重启调参代价高。

开源参考:Hutool 的线程池封装、Alibaba TransmittableThreadLocal(上下文传递)、DynamicTp(动态线程池治理)。

误区 修正
用 Executors.newFixedThreadPool 无界队列风险,改显式构造 ThreadPoolExecutor
每个功能一个线程 无上限、无复用,规模一大即失控
只看活跃线程不看等待时间 关注任务排队时长 P99,它是用户可感知的延迟
线程池大小当性能开关 根因常在下游容量与任务耗时
  • 《Java 并发编程实战》第 8 章(线程池的使用与调优)
  • Brian Goetz 关于线程池的文章(Java SE 5 引入背景)
  • 利特尔法则(Little’s Law)在容量规划中的应用