这里写自定义目录标题
- 线程池使用总结
-
- 一、项目线程池全景盘点
-
- [1.1 明细表](#1.1 明细表)
- [1.2 数量级估算](#1.2 数量级估算)
- 二、总结三个问题
-
- Q1:这么多线程池会给服务器造成压力吗?
- Q2:任务执行完毕会不会自动关闭线程池?
- [Q3:没开 `allowCoreThreadTimeOut` 的池有什么影响?哪种方式更好?](#Q3:没开
allowCoreThreadTimeOut的池有什么影响?哪种方式更好?)
- 三、沉淀的通用知识点
-
- [3.1 ThreadPoolExecutor 执行顺序(背下来)](#3.1 ThreadPoolExecutor 执行顺序(背下来))
- [3.2 关键参数语义速查](#3.2 关键参数语义速查)
- [3.3 优雅关闭模板(项目内通用写法)](#3.3 优雅关闭模板(项目内通用写法))
- [3.4 其他经验](#3.4 其他经验)
- 四、一句话结论
线程池使用总结
一、项目线程池全景盘点
1.1 明细表
| # | 线程池 | 所在类 | 用途 | core/max | 队列 | keepAlive | allowCoreThreadTimeOut | 拒绝策略 | 任务跑完后 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | BIND_SUPPLEMENT_EXECUTOR | ConfigBindSupplementServiceImpl | 绑定补充表历史同步(调getSerialBind+写JSONL文件) | 5/10 | LinkedBlockingQueue(100) | 60s | ✅ | CallerRunsPolicy | 60s后线程全部退出归0 |
| 2 | EXT_SUPPLEMENT_EXECUTOR | ConfigExtSupplementServiceImpl | ext补充表历史同步 | 5/10 | 同上 | 60s | ✅ | 同上 | 同上 |
| 3 | SYNC_EXECUTOR | MesSyncRecordsServiceImpl | 失败重试同步 | 5/10 | 同上 | 60s | ✅ | 同上 | 同上 |
| 4 | SYNC_EXECUTOR | HyperMesLevelSyncRecordServiceImpl | 待同步任务处理 | 5/10 | 同上 | 60s | ✅ | 同上 | 同上 |
| 5 | EXT_SYNC_EXECUTOR | HyperMesExtLevelSyncRecordServiceImpl | 扩展信息同步 | 5/10 | 同上 | 60s | ✅ | 同上 | 同上 |
| 6 | cellExtSyncExecutor | SyncServiceImpl | 电芯扩展信息同步 | 5/10 | 同上 | 60s | ✅ | 同上 | 同上 |
| 7 | EXECUTOR (mes-sync-*) | HyperTraceNewMesScheduleServiceImpl | 定时同步任务(PriorityTask实现Comparable,按优先级出队) | 5/10 | PriorityBlockingQueue(无界) | 60s | ❌ | CallerRunsPolicy | 5个核心线程常驻(有@PreDestroy) |
| 8 | mesThreadPool | ThreadPoolConfig @Bean | MES专用(唯一使用方:HyperMesEmuPcsBindServiceImpl,EMU/PCS绑定批量同步) | min(CPU,8) / core×2 | LinkedBlockingQueue(500) | 60s | ❌ | CallerRunsPolicy | 用到过的核心线程常驻(懒创建,不用到不建) |
| 9 | mqttExecutor / uploadExecutor | ThreadPoolTaskExecutorConfig(Spring ThreadPoolTaskExecutor) | MqttDataServiceImpl 实时MQTT消息处理 / 文件上传处理 | 4/8 | 1000 | 20s | ❌ | CallerRunsPolicy | 核心线程常驻(Spring生命周期管理) |
1.2 数量级估算
- 全部同时满载的峰值(理论极端值,实际不会发生):6×10 + 10 + 16 + 8 + 8 ≈ 100 根线程
- 全部空闲时的驻留 :6×0(开了超时回收)+ 5 + 0~8 + 4 + 4 ≈ 13~17 根空闲线程
- 定时任务时间错开的前提下,同一时刻通常只有 1~2 个池活跃(5~10 根线程在干活)
二、总结三个问题
Q1:这么多线程池会给服务器造成压力吗?
不会,三个原因:
- 空闲线程的代价可以忽略 :空闲线程 park 在队列
take()上,0 CPU;内存只有线程栈(约 1MB/线程的虚拟内存预留,实际 RSS 更小)。十几个空闲线程对服务器无感。 - 6 个跑批池都开了
allowCoreThreadTimeOut(true):任务全部结束后 60 秒连核心线程也自动销毁,池回到 0 线程状态,只剩一个几 KB 的空壳对象。任务跑完一分钟后用jstack <pid>验证,pool-x-thread-*都会消失。 - 真正的压力只来自"同时活跃"的线程:错峰调度下同一时刻一般只有一两个池在工作,任务内容是调 HTTP 接口 + 追加本地 JSONL 文件,数据库占用很少(批任务每批只做一次 IN 点查)。
另外所有池统一用 CallerRunsPolicy ------队列满时由提交任务的线程自己执行,形成天然反压(backpressure):任务永远不会抛 RejectedExecutionException,也不会无限堆积,这是防过载的关键保险丝。
Q2:任务执行完毕会不会自动关闭线程池?
分两层理解:
| 层面 | 行为 | 说明 |
|---|---|---|
| 线程 | 会回收 | 开了 allowCoreThreadTimeOut(true) 的池:最后一个任务结束 60s 后所有线程(含核心线程)退出归 0。没开的池:非核心线程按 keepAlive 回收,核心线程常驻 |
| 线程池对象 | 不销毁,也不应该 | 它是 static final 常驻的"池子",线程走了壳还在,下次任务来了重新拉线程。复用池对象正是线程池的设计意图,频繁建池/销毁反而更贵。空池只占几 KB 内存,不是泄漏 |
项目下线 走 @PreDestroy 优雅关闭链路(停机日志已验证):
shutdown() // 不再接收新任务
→ awaitTermination(60s) // 给在途任务最多60秒跑完
→ shutdownNow() // 强制中断
→ awaitTermination(30s) // 再兜底等30秒
Spring 管理的 mqttExecutor/uploadExecutor 由 Spring 生命周期自动关闭(停机日志里的 Shutting down ExecutorService 'mqttExecutor' 即是)。
停机时在途任务失败的处理 :曾观察到停机瞬间日志 WARN | pool-8-thread-9 | 编码 xxx 绑定补充表数据保存失败 紧跟 SpringContextShutdownHook 关池日志------这是停机打断了在途任务。该场景由断点续传兜底 :游标按批落 Redis,整批没跑完游标就没推进,下次启动自动重做该批,不丢数据。
Q3:没开 allowCoreThreadTimeOut 的池有什么影响?哪种方式更好?
影响:只有常驻内存,没有任何功能影响。
| 维度 | 影响 |
|---|---|
| CPU | 0 |
| 内存 | mes-sync 常驻 5 根、mesThreadPool 常驻 ≤8 根,合计几 MB 栈预留 |
| 功能 | 无------任务照常执行,不多不少不慢 |
| 下线关闭 | 无------mes-sync 有自己的 @PreDestroy,Spring 池由 Spring 关闭 |
换来的是"线程常温 ":任务到来不用新建线程,省的是 0.1ms 级别的建线程开销。
哪种更好------按池的任务类型分场景,不是普遍更好:
| 池类型 | 建议 | 原因 |
|---|---|---|
| 批处理/跑批池(本项目的 6 个 sync 池、mes-sync、mesThreadPool) | 开 allowCoreThreadTimeOut(true) 更干净 |
任务突发(一天跑几次),跑完归 0、footprint 最小;下次任务重建 5 根线程约 1ms,相对跑批任务本身(秒~分钟级)完全无感 |
| 实时事件驱动池(mqttExecutor、uploadExecutor) | 保留核心线程常驻是惯例 | 消息/上传事件到达时希望有线程立即可用;虽然新建线程也只要 0.1ms,但实时链路一般选择常温 |
一个已知 trade-off :任务间隔略大于 keepAlive 时(如每 70 秒一次任务、keepAlive 60 秒),会出现"每次跑批都重建线程"的 churn。对本项目的跑批频率(小时/天级)无感,知道即可。
落地建议 :mes-sync 和 mesThreadPool 可各补一行 allowCoreThreadTimeOut(true) 与其他 6 个对齐;mqtt/upload 不动。不改也完全没问题(差异只是十几根空闲线程、几 MB 内存)。
三、沉淀的通用知识点
3.1 ThreadPoolExecutor 执行顺序(背下来)
提交任务
→ 线程数 < corePoolSize:新建核心线程执行
→ 达到 core:任务入队列
→ 队列满 且 线程数 < maxPoolSize:新建非核心线程执行
→ 队列满 且 达到 max:走拒绝策略
3.2 关键参数语义速查
| 参数 | 语义 | 本项目取值惯例 |
|---|---|---|
| corePoolSize | 常驻线程数 | 5(跑批类)/ 4(实时类) |
| maxPoolSize | 峰值线程数 | core×2 |
| keepAliveTime | 非核心线程空闲存活时间 | 60s(Spring 池 20s) |
| allowCoreThreadTimeOut(true) | 核心线程也参与超时回收,池可归 0 | 跑批池开、实时池不开 |
| workQueue | 有界队列防堆积 | 100~1000 |
| RejectedExecutionHandler | 队列满后的处置 | 统一 CallerRunsPolicy(反压) |
3.3 优雅关闭模板(项目内通用写法)
java
@PreDestroy
public void shutdown() {
EXECUTOR.shutdown();
try {
if (!EXECUTOR.awaitTermination(60, TimeUnit.SECONDS)) {
EXECUTOR.shutdownNow();
if (!EXECUTOR.awaitTermination(30, TimeUnit.SECONDS)) {
log.error("线程池强制关闭后仍有任务未完成");
}
}
} catch (InterruptedException e) {
EXECUTOR.shutdownNow();
Thread.currentThread().interrupt();
}
}
3.4 其他经验
- 不建议把多个业务池合并成一个共享大池:现在的按业务隔离是合理的------一个慢任务(如某工厂 MES 接口慢)占满共享池会饿死其他业务。
- 验证手段 :任务跑完 60s 后
jstack <pid> | grep pool-应看不到对应线程;停机时看SpringContextShutdownHook的关池日志确认@PreDestroy生效。 - 长跑批任务要配断点续传(游标按批落 Redis),停机打断在途任务后重启才能无损续跑------本项目的同步类服务都按这个模式实现。
四、一句话结论
本项目线程池规模(空闲驻留 13~17 根、错峰下同时活跃 5~10 根)对服务器无实质压力;跑批池开
allowCoreThreadTimeOut(true)跑完归 0、实时池保留核心线程常驻,是按任务类型做的正确取舍;停机走@PreDestroy优雅关闭 + 断点续传兜底,整条链路已验证闭环。