SpringBoot源码解析-Scheduled定时器的原理「建议收藏」

编程文档 (63) 2023-08-13 11:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说SpringBoot源码解析-Scheduled定时器的原理「建议收藏」,希望能够帮助你!!!。

定时器的使用

可以参考下肥朝大佬的文章 原理暂且不谈,定时器你当真会用? 写的很形象。

定时器原理剖析

定时器的基础是jdk中的工具类ScheduledThreadPoolExecutor,想要了解springboot定时器的原理,先得了解ScheduledThreadPoolExecutor的原理。

该类中有三个主要的方法:

  1. schedule(...)
  2. scheduleWithFixedDelay(...)
  3. scheduleAtFixedRate(...)

我们先简单回顾下这三个方法。

schedule方法

schedule方法的作用是提供一个延时执行的任务,该任务只会执行一次。该方法的三个参数如下

schedule(Runnable command,   long delay,   TimeUnit unit)

command为需要执行的任务,delay和unit组合起来使用,表示延时的时间。

    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
        //校验参数
        if (command == null || unit == null)
            throw new NullPointerException();
        //任务转换
        RunnableScheduledFuture<?> t = decorateTask(command,
            new ScheduledFutureTask<Void>(command, null,
                                          triggerTime(delay, unit)));
        //添加任务到延时队列
        delayedExecute(t);
        return t;
    }

首先看一下任务转换的逻辑:

    //将延时的时间加上现在的时间,转化成真正执行任务的时间
    private long triggerTime(long delay, TimeUnit unit) {
        return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
    }

    //将任务转化为ScheduledFutureTask对象
    ScheduledFutureTask(Runnable r, V result, long ns) {
        super(r, result);
        this.time = ns;
        //period为0表示只执行一次
        this.period = 0;
        this.sequenceNumber = sequencer.getAndIncrement();
    }

接下来就是添加进任务队列:

    private void delayedExecute(RunnableScheduledFuture<?> task) {
        //检查任务状态
        if (isShutdown())
            reject(task);
        else {
            //添加进队列
            super.getQueue().add(task);
            //在执行之前,再次检查任务状态
            if (isShutdown() &&
                !canRunInCurrentRunState(task.isPeriodic()) &&
                remove(task))
                task.cancel(false);
            else
                //检查是否有线程在处理任务,如果工作线程数少于核心线程数,会新建worker。
                ensurePrestart();
        }
    }

添加的逻辑看完了,现在看一下加入队列后是如何执行的:

//worker线程会调用刚刚封装好的ScheduledFutureTask对象的run方法
public void run() {
    //判断period是否是0
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        //在schedule方法中period是0,进入父类的run方法,run方法中
        //会调用我们传入的任务
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
}

schedule方法的执行逻辑大致如上,schedule方法只执行一次。

scheduleWithFixedDelay方法

该方法的作用是在任务执行完成后,经过固定延时时间再次运行。

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit) {
        //校验参数
        if (command == null || unit == null)
            throw new NullPointerException();
        if (delay <= 0)
            throw new IllegalArgumentException();
        //将任务转化为ScheduledFutureTask对象,注意这个地方period不是0了!
        ScheduledFutureTask<Void> sft =
            new ScheduledFutureTask<Void>(command,
                                          null,
                                          triggerTime(initialDelay, unit),
                                          unit.toNanos(-delay));
        //将outerTask设置为自己
        RunnableScheduledFuture<Void> t = decorateTask(command, sft);
        sft.outerTask = t;
        //添加进延时队列
        delayedExecute(t);
        return t;
    }

和schedule方法稍有不同,一个是period不在是0,而是小于0,还有就是将outerTask设置为自己。

添加进任务队列的逻辑都是一样的,所以直接看执行逻辑:

public void run() {
    //这个地方periodic是true了
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        ScheduledFutureTask.super.run();
    //所以会进入下面这个逻辑
    else if (ScheduledFutureTask.super.runAndReset()) {
        //设置下一次任务时间
        setNextRunTime();
        //将自己再次添加进队列
        reExecutePeriodic(outerTask);
    }
}
//period是小于0的,注意这个地方大于0和小于0逻辑上的区别
private void setNextRunTime() {
    long p = period;
    if (p > 0)
        //大于0的话,是用上次执行的时间,加上延时时间算出下次执行的时间
        time += p;
    else
        //小于0的话,是用当前时间,加上延时时间,算出下次执行时间
        time = triggerTime(-p);
}

scheduleAtFixedRate方法

这个方法和上一个方法几乎一样,唯一的区别就是他的period是大于0的,所以延时时间按照大于0来计算。


springboot中定时器的原理

了解完ScheduledThreadPoolExecutor的基础原理后,我们来看一下springboot中定时任务的调度。springboot定时任务调度的基础是ScheduledAnnotationBeanPostProcessor类,查看继承体系发现该类实现了BeanPostProcessor接口,所以进入该类的postProcessAfterInitialization方法。

	public Object postProcessAfterInitialization(Object bean, String beanName) {
		if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
				bean instanceof ScheduledExecutorService) {
			// Ignore AOP infrastructure such as scoped proxies.
			return bean;
		}

		Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
		if (!this.nonAnnotatedClasses.contains(targetClass)) {
			//查找被Scheduled注解标注的类
			Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
					(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
						Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
								method, Scheduled.class, Schedules.class);
						return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
					});
			if (annotatedMethods.isEmpty()) {
				this.nonAnnotatedClasses.add(targetClass);
				if (logger.isTraceEnabled()) {
					logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
				}
			}
			else {
				// Non-empty set of methods
				//如果被Scheduled注解标注,就执行processScheduled方法。
				annotatedMethods.forEach((method, scheduledMethods) ->
						scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
				if (logger.isTraceEnabled()) {
					logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
							"': " + annotatedMethods);
				}
			}
		}
		return bean;
	}
	
	//以cron模式来解析一下processScheduled方法
	protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
		try {
			Runnable runnable = createRunnable(bean, method);
			boolean processedSchedule = false;
			...
			// 解析注解里的属性
			String cron = scheduled.cron();
			if (StringUtils.hasText(cron)) {
				String zone = scheduled.zone();
				if (this.embeddedValueResolver != null) {
					cron = this.embeddedValueResolver.resolveStringValue(cron);
					zone = this.embeddedValueResolver.resolveStringValue(zone);
				}
				if (StringUtils.hasLength(cron)) {
					Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
					processedSchedule = true;
					if (!Scheduled.CRON_DISABLED.equals(cron)) {
						TimeZone timeZone;
						if (StringUtils.hasText(zone)) {
							timeZone = StringUtils.parseTimeZoneString(zone);
						}
						else {
							timeZone = TimeZone.getDefault();
						}
						//将封装好的任务存储起来
						tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
					}
				}
			}
			...
			// Finally register the scheduled tasks
			synchronized (this.scheduledTasks) {
				Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
				//根据bean分类,将每个bean的定时任务存进scheduledTasks
				regTasks.addAll(tasks);
			}
		}
		...
	}

	public ScheduledTask scheduleCronTask(CronTask task) {
		ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
		boolean newTask = false;
		if (scheduledTask == null) {
			//根据task,新建一个ScheduledTask
			scheduledTask = new ScheduledTask(task);
			newTask = true;
		}
		if (this.taskScheduler != null) {
			scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
		}
		else {
			//根据定时任务种类的区别存储task
			addCronTask(task);
			this.unresolvedTasks.put(task, scheduledTask);
		}
		return (newTask ? scheduledTask : null);
	}

在postProcessAfterInitialization方法中,spring主要就是解析注解,并将根据注解生成相应的延时任务。那么现在解析好了,也存储好了,执行的地方在哪里呢?在一次查看该类的继承体系,发现该类还实现了ApplicationListener接口,所以进入onApplicationEvent方法。

	public void onApplicationEvent(ContextRefreshedEvent event) {
		if (event.getApplicationContext() == this.applicationContext) {
			finishRegistration();
		}
	}

	private void finishRegistration() {
		...
		//上面一大段都是寻找taskScheduler类的,如果没有设置的话这边是找不到的
		this.registrar.afterPropertiesSet();
	}

	public void afterPropertiesSet() {
		scheduleTasks();
	}

	protected void scheduleTasks() {
		//没有自定义配置就使用默认配置
		if (this.taskScheduler == null) {
			//默认的执行器只有一个线程使用的时候要注意一下
			this.localExecutor = Executors.newSingleThreadScheduledExecutor();
			this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
		}
		if (this.triggerTasks != null) {
			for (TriggerTask task : this.triggerTasks) {
				addScheduledTask(scheduleTriggerTask(task));
			}
		}
		if (this.cronTasks != null) {
			for (CronTask task : this.cronTasks) {
				addScheduledTask(scheduleCronTask(task));
			}
		}
		if (this.fixedRateTasks != null) {
			for (IntervalTask task : this.fixedRateTasks) {
				addScheduledTask(scheduleFixedRateTask(task));
			}
		}
		if (this.fixedDelayTasks != null) {
			for (IntervalTask task : this.fixedDelayTasks) {
				addScheduledTask(scheduleFixedDelayTask(task));
			}
		}
	}

在该方法中,清晰的看到了定时任务调用的过程triggerTasks好像不是通过注解进来的,这个先不管。我们可以看一下另外三个的执行。

cron执行逻辑

	public ScheduledTask scheduleCronTask(CronTask task) {
		ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
		...
		//这个地方taskScheduler已经有默认值了
		if (this.taskScheduler != null) {
			scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
		}
		...
		return (newTask ? scheduledTask : null);
	}

	public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
		try {
			...
			else {
				...
				//新建了一个ReschedulingRunnable对象,调用schedule方法。
				return new ReschedulingRunnable(task, trigger, this.scheduledExecutor, errorHandler).schedule();
			}
		}
		...
	}

	//新建一个ReschedulingRunnable对象,这个对象也实现了runnable接口
	public ReschedulingRunnable(
			Runnable delegate, Trigger trigger, ScheduledExecutorService executor, ErrorHandler errorHandler) {

		super(delegate, errorHandler);
		this.trigger = trigger;
		this.executor = executor;
	}

	public ScheduledFuture<?> schedule() {
		synchronized (this.triggerContextMonitor) {
			this.scheduledExecutionTime = this.trigger.nextExecutionTime(this.triggerContext);
			if (this.scheduledExecutionTime == null) {
				return null;
			}
			//计算下次执行时间
			long initialDelay = this.scheduledExecutionTime.getTime() - System.currentTimeMillis();
			//将自己传入执行器,也就是调用自己的run方法
			this.currentFuture = this.executor.schedule(this, initialDelay, TimeUnit.MILLISECONDS);
			return this;
		}
	}

	public void run() {
		Date actualExecutionTime = new Date();
		//执行我们定义的定时任务
		super.run();
		Date completionTime = new Date();
		synchronized (this.triggerContextMonitor) {
			Assert.state(this.scheduledExecutionTime != null, "No scheduled execution");
			//更新时间
			this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
			if (!obtainCurrentFuture().isCancelled()) {
				//在次调用schedule方法
				schedule();
			}
		}
	}

在上面我们分析执行器逻辑的时候,知道执行器的schedule方法只会执行一次,所以springboot在这个地方使用互相调用的方法,来达到定时循环的目的。所以这个方法中,关键的就是时间的更新。

	public Date nextExecutionTime(TriggerContext triggerContext) {
		//获取上一次任务完成时间
		Date date = triggerContext.lastCompletionTime();
		if (date != null) {
			//获取上一次任务执行的时间
			Date scheduled = triggerContext.lastScheduledExecutionTime();
			//比较两次时间,使用后者生成新的执行时间
			if (scheduled != null && date.before(scheduled)) {
				date = scheduled;
			}
		}
		else {
			//初始化的时候直接使用当前时间
			date = new Date();
		}
		return this.sequenceGenerator.next(date);
	}

cron模式每次根据上次执行时间和上次完成时间更后面的生成新的时间,结合肥朝的文章应该可以理解这种模型。不过这个地方我也不太明白什么情况下完成时间会在执行时间的前面。反正就是根据最新的时间生成新的时间就是。

剩下的两个执行逻辑

	public ScheduledTask scheduleFixedRateTask(IntervalTask task) {
		FixedRateTask taskToUse = (task instanceof FixedRateTask ? (FixedRateTask) task :
				new FixedRateTask(task.getRunnable(), task.getInterval(), task.getInitialDelay()));
		return scheduleFixedRateTask(taskToUse);
	}

	public ScheduledTask scheduleFixedRateTask(FixedRateTask task) {
		...
			scheduledTask.future =
					this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getInterval());
		...
		return (newTask ? scheduledTask : null);
	}

	public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {
		try {
			return this.scheduledExecutor.scheduleAtFixedRate(decorateTask(task, true), 0, period, TimeUnit.MILLISECONDS);
		}
		catch (RejectedExecutionException ex) {
			throw new TaskRejectedException("Executor [" + this.scheduledExecutor + "] did not accept task: " + task, ex);
		}
	}

另外两个模式就是执行ScheduledThreadPoolExecutor对应的方法了,关键还是时间的逻辑,时间的生成逻辑上面已经给出来了,就是根据period大于0还是小于0来生成的。


返回目录

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

发表回复