Bootstrap

Spring源码 | SchedulingConfigurer / TaskScheduler / ThreadPoolTaskScheduler 自定义任务调度器实现与全链路源码分析

ThreadPoolTaskScheduler 是 Spring Framework 提供的一个任务调度器,用于在多线程环境下执行定时任务。它基于线程池来管理任务的执行,可以根据配置的参数来控制线程池的大小、任务队列的大小等,从而灵活地控制任务的并发执行情况。

ThreadPoolTaskScheduler 的运行原理主要包括以下几个关键点:

线程池管理:ThreadPoolTaskScheduler 使用 ThreadPoolExecutor 作为底层的线程池管理器。通过配置 corePoolSize 和 maxPoolSize 可以控制线程池的核心线程数和最大线程数。当任务数超过核心线程数时,新任务会被放入任务队列中等待执行,直到队列满后才会创建新的线程执行任务,直到达到最大线程数。

任务调度:ThreadPoolTaskScheduler 使用 ScheduledExecutorService 来实现任务的调度功能。它可以执行延迟任务和周期性任务,通过调用 schedule 和 scheduleAtFixedRate 等方法来提交任务,然后由线程池中的线程来执行。

任务队列管理:ThreadPoolTaskScheduler 使用 BlockingQueue 来管理任务队列。可以通过配置 queueCapacity 来控制任务队列的大小,以避免任务过多导致内存溢出或性能下降的问题。

异常处理:ThreadPoolTaskScheduler 提供了异常处理机制,可以通过设置 errorHandler 属性来处理任务执行过程中的异常情况。默认情况下,异常会被打印到日志中,但也可以自定义实现 ErrorHandler 接口来处理异常。

1. SchedulingConfigurer

在开始介绍ThreadPoolTaskScheduler ,先从SchedulingConfigurer入手,任何自定义定时任务调度器,都要实现这个接口。

1.1 SchedulingConfigurer包括了一个方法configureTasks

1.2 configureTasks 传入了一个ScheduledTaskRegistrar类型的对象,,通过这个对象可以配置任务调度器的各种属性。ScheduledTaskRegistrar包含了一个方法setScheduler。

1.3 可以调用 setScheduler 方法设置自定义的任务调度器实现,也可以调用 addTriggerTask 和 addCronTask 方法添加触发器任务和 cron 表达式任务。

1.4 setScheduler 方法传入了一个当前类的TaskScheduler对象。

2. TaskScheduler

askScheduler 接口是 Spring Framework 提供的一个任务调度器接口,用于管理和执行任务。

2.1 TaskScheduler接口的私有方法

schedule(Runnable task, Trigger trigger):使用给定的触发器安排一个单次执行的任务。触发器定义了任务的执行时间。

schedule(Runnable task, Date startTime):安排一个在给定的开始时间执行的任务。

scheduleAtFixedRate(Runnable task, Date startTime, long period):安排一个在给定的开始时间开始执行,然后以固定速率执行的任务。

scheduleAtFixedRate(Runnable task, long period):安排一个以给定的固定速率执行的任务,初始延迟为0。

scheduleWithFixedDelay(Runnable task, Date startTime, long delay):安排一个在给定的开始时间开始执行,然后在每次执行结束和下一次执行开始之间存在给定延迟的任务。

scheduleWithFixedDelay(Runnable task, long delay):安排一个在给定的初始延迟后开始执行,并在每次执行结束和下一次执行开始之间存在给定延迟的任务。

这些私有方法是用于实现任务调度功能的核心方法,通过这些方法可以实现不同类型的任务调度。

注:这里涉及到一个具体的方法schedule,这里在后续实现类会介绍到。

2.2 TaskScheduler接口实现类

常见的几种 TaskScheduler 接口的实现方法包括:

ThreadPoolTaskScheduler:基于线程池的任务调度器实现,可以通过配置线程池的参数来控制任务的并发执行情况。

ConcurrentTaskScheduler:简单的任务调度器实现,直接使用当前线程来执行任务,适用于不需要并发执行的任务场景。

ScheduledExecutorTaskScheduler:基于 ScheduledExecutorService 的任务调度器实现,类似于 ThreadPoolTaskScheduler,但没有线程池的管理功能。

CustomTaskScheduler:自定义的任务调度器实现,通过实现 TaskScheduler 接口的方法来实现自定义的调度逻辑,可以根据具体的业务需求来定制任务调度器的行为。
 

3. ThreadPoolTaskScheduler

3.1 ThreadPoolTaskScheduler继承了ConcurrentTaskExecutor

3.2 该类当中有一个私有成员scheduledExecutor,他的初始化如下:

3.3 继续跟踪:

3.4 可以看到,最后初始化了一个指定核心线程数的线程池,任务调度就是通过set线程池变量来实现的。

4. 自定义任务调度demo

以上介绍了SchedulingConfigurer / TaskScheduler / ThreadPoolTaskScheduler源码调用流程,以下是一个简单的示例,展示了如何实现 SchedulingConfigurer 接口来配置任务调度器。

@Configuration
@EnableScheduling
public class MySchedulingConfigurer implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.addTriggerTask(
            () -> {
                // 执行任务逻辑
            },
            triggerContext -> {
                // 定义任务触发规则
                CronTrigger cronTrigger = new CronTrigger("0 0/5 * * * ?");
                return cronTrigger.nextExecutionTime(triggerContext);
            }
        );
    }

    @Bean(destroyMethod="shutdown")
    public ThreadPoolTaskScheduler taskExecutor() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("my-scheduled-task-");
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setAwaitTerminationSeconds(60);
        return scheduler;
    }
}

5 如何提交任务(重点★★★★★)

5.1 ok,还记得2.1节,在TaskScheduler接口当中,有一个方法schedule吗!

我们还是以ThreadPoolTaskScheduler实现类为例。

5.2 追踪开始

注意,这里调用了ReschedulingRunnable的schedule方法!!!

5.3 观察schedule的实现过程。

this.trigger.nextExecutionTime(this.triggerContext);根据当前的触发器 (trigger) 和触发器上下文 (triggerContext) 计算下一次任务执行的时间,并将结果赋值给 scheduledExecutionTime。

整个方法是根据触发器计算下一次任务的执行时间,然后使用线程池调度器安排任务在计算出的时间执行

5.4 schedule方法下面,重写了run方法。

记录任务实际执行的时间。
调用父类的 run 方法,执行任务的主要逻辑。

记录任务执行完成的时间。

使用 triggerContextMonitor 对象进行同步,确保多线程环境下的安全访问。

this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);更新触发器上下文 (triggerContext),将预定执行时间、实际执行时间和完成时间传入,以便后续触发器的计算和任务状态的更新。

if (!obtainCurrentFuture().isCancelled()) { schedule(); }:如果当前任务的 ScheduledFuture 对象未被取消,则重新调度任务,即再次调用 schedule() 方法安排下一次执行。

这个方法的作用是在任务执行完成后,更新触发器上下文,并根据当前任务的状态决定是否重新安排下一次执行。

5.5 父类的 run 方法

这个类的作用是提供一个可以处理错误的 Runnable 包装类,通过在 run 方法中捕获异常并调用错误处理器来处理异常,从而保证任务执行过程中的异常不会导致整个程序崩溃,并且可以在异常发生时进行相应的处理。

我们在实际实现过程中,可以自定义自己的Runnable 实现方法。

public class MyRunnable implements Runnable {

    private String message;

    public MyRunnable(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        System.out.println("Executing MyRunnable with message: " + message);
        // 添加自己的逻辑代码
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable("Hello, world!");
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

6 自定义任务调度器实现全流程demo(仅作参考)

@Configuration
@EnableScheduling
public class MySchedulingConfigurer implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.addTriggerTask(
            new MyRunnable("Hello, world!"),
            triggerContext -> {
                // 定义触发器,每隔一段时间执行一次
                CronTrigger cronTrigger = new CronTrigger("0/5 * * * * ?");
                return cronTrigger.nextExecutionTime(triggerContext);
            }
        );
    }

    @Bean(destroyMethod="shutdown")
    public ThreadPoolTaskScheduler taskExecutor() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        return scheduler;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(MySchedulingConfigurer.class);
        context.refresh();
    }
}

看了一下午spring源码,轮子造起来!!!

;