线程池踩坑之一:将其放在类的成员变量

引言

大家好呀!自从上次更新以来,已经有几个月没有更新了。这次给大家更新一个我在工作中看到老旧项目的雷点,这种问题没处理好,轻则导致内存占用逐渐升高,重则直接OOM,应用直接宕机🥲

背景

公司的定时任务是使用Quartz来调用的,而当初为了快速开发,赶工期进度,所以整个项目的代码规范和CR就不是做的很好。这为后期维护和继续开发埋下了雷点。

某位故人在开发定时任务模块的时候,喜欢把线程池放在整个定时任务类下面作为成员变量使用,但这样有非常大的隐患!!!

例如下面的代码

java 复制代码
@DisallowConcurrentExecution
public class MyJob implements Job {

    private final ThreadPoolExecutor threadPoolExecutor =
        new ThreadPoolExecutor(
            2 * Runtime.getRuntime().availableProcessors(),
            5 * Runtime.getRuntime().availableProcessors(),
            60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );

    @Override
    public void execute(JobExecutionContext context) {
        // 使用 threadPoolExecutor
    }
}

隐患分析

Quartz维度

Quartz 默认的 JobFactoryorg.quartz.simpl.SimpleJobFactory)在每次触发时,都会重新实例化一个新的 Job 对象 ,然后调用它的 execute 方法。

所以如果你不做特殊配置,那么每一次任务触发,都会创建一个新的任务类实例

为什么要这样设计?

  • 避免线程安全问题。因为 Quartz 的任务可能并行执行,如果共享同一个 Job 实例会出现状态混乱(上下文与数据等)。
  • Job 类默认是 无状态的(stateless) ,每次用完丢掉

特殊情况?如果你的 Job 类加了 @DisallowConcurrentExecution 注解,Quartz 会确保同一时间不会并发执行同一个 JobDetail 的实例,但它依然是每次触发都会创建新对象

如果Quartz 调用它的 execute 方法执行完之后,这个 Job 对象就不再被 Quartz 持有引用,没有引用 → 会被 JVM 的垃圾回收器回收,所以:Job 对象里定义的成员变量也会跟着消亡(只要你没有把它们存到静态变量、单例 bean、ThreadLocal 等地方)

放在我们这里的场景来说,我们这里的成员变量是一个线程池,但是如果每次都new一个任务实例出来的时候,如果它不是静态成员变量,就每次都会重新创建出来一个线程池来使用。那就完全没有使用池化这个思想,同时也是完全浪费了线程池的核心作用。最可怕的是每一次都会以线程池维度而不是单个线程增加线程,最严重的就会导致整个应用OOM! 😭

类的成员变量维度

java 复制代码
private static final ThreadPoolExecutor threadPoolExecutor =
        new ThreadPoolExecutor(
            2 * Runtime.getRuntime().availableProcessors(),
            5 * Runtime.getRuntime().availableProcessors(),
            60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1000),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );

这串代码与刚才不同的就是给线程池这个成员变量加了一个static关键字。

static 在这里的作用

  • static 成员变量属于 类本身,而不是某个对象实例。
  • 因此无论 Quartz new 出多少个 Job 实例,它们都指向同一个 threadPoolExecutor 对象
  • 也就是说,线程池只会在类加载时初始化一次,而不会随着 Job 实例的创建而重复 new

如果不加static关键字,每一次都new一个线程池出来,不复用同一个线程池,导致线程数蹭蹭上涨!

生命周期

  • 这个线程池会一直存在,直到:

    • JVM 进程结束,或者
    • 你主动调用 threadPoolExecutor.shutdown()
  • 所以它和 Job 实例的生命周期解耦,不会因为 Job 执行完而销毁

线程池维度

这个任务执行完了,并不代表这个线程池也会结束并回收。

MyJob 实例被 Quartz 调度器回收时,它持有的 threadPoolExecutor 成员变量引用会被清除,但线程池本身(ThreadPoolExecutor 对象)不会自动关闭或销毁,其内部的线程仍会继续存活,直到显式调用 shutdown() 或 JVM 退出

我们需要注意的是:对象回收 ≠ 资源释放

Java 的垃圾回收(GC)只负责回收堆内存中的对象实例 ,但不会自动关闭或释放该对象所持有的系统资源 ------ 如:

  • 文件句柄
  • 网络连接
  • 线程池中的线程
  • 数据库连接
  • 锁、信号量等

每一个被回收的任务实例的线程池所创建的线程依旧还存活着,内存泄漏最终会演变成内存溢出。

为什么之前没有发现这个问题?

之前为了快速开发,每次应用都会重新发版重启,而且定时任务执行频率不是很高,所以问题不是很明显。随着公司的业务发展,定时任务的个数与执行频率也在升高,在grafana看到线程数一直飙升,这个问题势必要进行解决。

解决之策

短期缓解------在每一个成员变量线程池加static

避免了每次调度都 new 一个线程池,线程数不会无限膨胀。Job 实例之间共享资源,更高效,基本不会出现线程数蹭蹭往上涨的问题。

这个方法只是短期的解决方案,并不长久。

长期方案------线程池统一交给容器管理

好处

为什么要交给 Spring 管理?

  • 生命周期可控:线程池由 Spring 容器创建和销毁,应用启动时初始化,关闭时自动优雅地 shutdown。
  • 资源复用:所有需要用线程池的 Job、Service 都能从容器里注入同一个池,避免重复造轮子。
  • 配置集中化:核心线程数、最大线程数、队列大小、拒绝策略等参数统一配置,方便调整。
  • 监控和管理:可以通过 Spring Boot Actuator 或自定义监控暴露线程池运行状态(活跃线程数、队列长度等)

对比

  • 非 static 成员变量:线程池随着 Job 实例频繁 new,泄漏风险大,线程数失控。
  • static 成员变量:同一个 Job 类共用一个池,但分散在多个类里不好统一管理,生命周期不可控。
  • 统一管理类(Manager) :集中管理,单例池,可控性好,最推荐

实现示例

可以根据不同的业务板块,线程池管理类可以有多个,单个管理类下面存放管理多个线程池,用于不同的任务执行。这样就可以统一管理了

java 复制代码
@Component
public class ThreadPoolManager {

    private final ThreadPoolExecutor executor;

    public ThreadPoolManager() {
        this.executor = new ThreadPoolExecutor(
                2 * Runtime.getRuntime().availableProcessors(),
                5 * Runtime.getRuntime().availableProcessors(),
                60, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    public ThreadPoolExecutor getExecutor() {
        return executor;
    }

    @PreDestroy
    public void shutdown() {
        executor.shutdown();
    }
}

总结

线程池作为成员变量或者在方法内创建线程池总是会有各种问题,在初期代码规范时就应该保证不能出现这样的代码,否则后期维护则极其困难与烦心!最好的方法就是交给容器统一管理生命周期,同时也方便使用❤️

相关推荐
心月狐的流火号2 小时前
Redis 的高性能引擎 Reactor 详解与基于 Go 手写 Redis
redis·后端
橙序员小站2 小时前
搞定系统设计题:如何设计一个支付系统?
java·后端·面试
Java水解2 小时前
Spring Boot + ONNX Runtime模型部署
spring boot·后端
Java水解2 小时前
Spring Security6.3.x使用指南
后端·spring
魂尾ac2 小时前
Django + Vue3 前后端分离技术实现自动化测试平台从零到有系列 <第一章> 之 注册登录实现
后端·python·django·vue
007php0073 小时前
Redis高级面试题解析:深入理解Redis的工作原理与优化策略
java·开发语言·redis·nginx·缓存·面试·职场和发展
CodeSaku3 小时前
是设计模式,我们有救了!!!(七、责任链模式:Chain of Responsibity)
后端
贵州数擎科技有限公司3 小时前
Go-zero 构建 RPC 与 API 服务全流程
后端