线程池使用总结

这里写自定义目录标题

线程池使用总结

一、项目线程池全景盘点

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:这么多线程池会给服务器造成压力吗?

不会,三个原因:

  1. 空闲线程的代价可以忽略 :空闲线程 park 在队列 take() 上,0 CPU;内存只有线程栈(约 1MB/线程的虚拟内存预留,实际 RSS 更小)。十几个空闲线程对服务器无感。
  2. 6 个跑批池都开了 allowCoreThreadTimeOut(true) :任务全部结束后 60 秒连核心线程也自动销毁,池回到 0 线程状态,只剩一个几 KB 的空壳对象。任务跑完一分钟后用 jstack <pid> 验证,pool-x-thread-* 都会消失。
  3. 真正的压力只来自"同时活跃"的线程:错峰调度下同一时刻一般只有一两个池在工作,任务内容是调 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-syncmesThreadPool 可各补一行 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 优雅关闭 + 断点续传兜底,整条链路已验证闭环。

相关推荐
SimonKing1 小时前
Java 图片处理还在用 ImageIO?这个库让你代码从 30 行变 3 行
java·后端·程序员
数据狐(Datafox)1 小时前
mercadolibre.item_get 工程实战:美客多商品详情API技术解析与落地应用
java·人工智能·mysql·json
~木雨1 小时前
Java 内部类系列④(收官):内部类底层与版本演进全梳理 —— 合成字段、nestmates 与 JDK18 优化,附全套面试背诵表
java·字节码·内部类·java 面试·nestmates·jdk18
吴声子夜歌1 小时前
Guava——反射
java·开发语言·guava
X1A0RAN2 小时前
Jenkins 流水线跨阶段的数据共享:让变量“动态更新“成为可能
java·servlet·jenkins
Rain的Java大神之路2 小时前
高并发下的热点账户余额扣减:Redis+Lua脚本实现无锁记账
java·spring boot·redis·后端·spring cloud·缓存·lua
随遇而安zx2 小时前
【地基篇】---JVM 类加载 源码解析(基于 Java 8)
java·jvm·python
liudashuang20172 小时前
【无标题】
java
这是程序猿2 小时前
高校宿舍信息管理系统小程序
java·小程序·毕业设计