Spring Boot场景下可控调度器的正确实现方式咨询
自定义可控调度器实现问题解答
你当前写的实现存在不少问题,无法直接满足作业启停、关闭的需求,具体问题点如下:
- 静态字段依赖Spring注入是典型的反模式:你把
Environment定义为静态变量,通过构造方法给静态字段赋值,一旦类加载时机早于Spring Bean实例化流程,调用INSTANCE()方法时env会直接为null抛空指针。而且Spring默认就是单例管理Bean,完全没必要自己写双重检查锁实现静态单例,属于多此一举还额外引入并发风险。 - 没有作业生命周期管理能力:你只做了线程池初始化,没有保存每个提交作业返回的
ScheduledFuture实例,根本无法单独停止指定作业,也没法追踪作业运行状态,自然实现不了细粒度的启停控制。 - 并发安全bug:双重检查锁实现单例时,
jobExecutor字段没有加volatile修饰,多线程场景下可能拿到未初始化完成的线程池实例。 - 线程池配置缺失:没有自定义线程命名工厂和拒绝策略,后续排查问题时无法从日志区分调度线程,默认的中止策略也容易在任务积压时直接抛未捕获异常。
推荐实现方式
完全不需要用静态方法获取实例,直接把调度器作为Spring单例Bean管理即可,参考代码:
@Slf4j @Service public class JobExecutor { // 存储作业ID和对应执行Future的映射,用于单独控制作业生命周期 private final ConcurrentHashMap<String, ScheduledFuture<?>> jobHolder = new ConcurrentHashMap<>(); private final ScheduledThreadPoolExecutor scheduler; private final AtomicInteger threadNum = new AtomicInteger(1); // 构造器直接注入配置,不需要静态字段 public JobExecutor(Environment env) { int coreSize = Integer.parseInt(Objects.requireNonNull(env.getProperty("scheduler.jobs"))); this.scheduler = new ScheduledThreadPoolExecutor(coreSize, r -> { Thread t = new Thread(r, "biz-scheduler-" + threadNum.getAndIncrement()); t.setDaemon(false); return t; }, new ThreadPoolExecutor.CallerRunsPolicy() ); // 绑定JVM关闭钩子,进程退出时自动释放线程池资源 Runtime.getRuntime().addShutdownHook(new Thread(this::shutdownAll)); } /** * 启动固定频率执行的作业 * @param jobId 作业唯一标识,重复传入会覆盖原有同ID作业 * @param task 作业执行逻辑 * @param initialDelay 首次执行延迟 * @param period 执行周期 * @param timeUnit 时间单位 */ public void scheduleAtFixedRate(String jobId, Runnable task, long initialDelay, long period, TimeUnit timeUnit) { // 先停止已存在的同ID作业,避免重复执行 stopJob(jobId); ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(task, initialDelay, period, timeUnit); jobHolder.put(jobId, future); log.info("作业[{}]启动完成,执行周期{} {}", jobId, period, timeUnit.name()); } /** * 停止单个作业 * @param jobId 作业唯一标识 */ public void stopJob(String jobId) { ScheduledFuture<?> future = jobHolder.remove(jobId); if (future != null && !future.isCancelled()) { // 参数传true会中断正在执行的作业,传false会等当前作业执行完再停止 future.cancel(true); log.info("作业[{}]已停止", jobId); } } /** * 关闭调度器,停止所有作业并释放线程资源 */ public void shutdownAll() { // 先停止所有已提交的作业 jobHolder.keySet().forEach(this::stopJob); scheduler.shutdown(); try { // 最多等待30秒让正在执行的作业跑完 if (!scheduler.awaitTermination(30, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } log.info("调度器已完全关闭"); } }
使用时直接注入JobExecutor实例,调用对应方法即可完成作业的启动、单个停止、全量关闭,完全满足细粒度控制需求。如果需要cron表达式支持,在启动作业时先把cron解析成下次执行延迟,包装任务每次执行完计算下次触发时间再提交即可,不需要依赖@Scheduled注解的能力。
关于之前Guava AbstractScheduledService无法正常运行的问题,绝大多数场景是漏了调用startAsync()触发服务启动,或是服务实例没有交给Spring管理导致生命周期和容器不对齐。这个类本身功能是正常的,但对于Spring Boot项目来说,直接基于JDK原生的ScheduledThreadPoolExecutor封装更轻量,不需要额外引入Guava依赖,排查问题也更简单。
内容的提问来源于stack exchange,提问作者ahrooran
相关产品推荐
相关产品推荐

