线程池与队列
用途:配置线程池参数、排查线程池导致的堆积与 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)在容量规划中的应用