Springboot @Scheduled 定时原理简析
本文最后更新于:2024年3月18日 晚上
前情提要
众所周知,Springboot
中可以用@Scheduled
开启定时任务
这个时候有人就说了:定时任务有什么好讲的,不就是定时の任务嘛
有点道理,干讲没有什么意思,我们来试想这样一种场景:
- 假如有一个定时任务,每小时触发一次,但是系统(
Windows
)休眠了6个小时
那么:这个定时任务会在开机后立刻被触发吗,触发几次呢?
正片叠底
好的,希望这个问题让你提起了一点点兴趣
@Scheduled
一共有三种定时方式:
cron:定义了一个如何循环的时间模式,比如“每天凌晨1点执行”,当然也可以定义为“每个小时的0分执行”;
cron
表达式由六个字段组成:[秒 分 时 日 月 周]这么说可能比较抽象,我们来看个例子:
- “0 * * * * *”:每分钟的0秒都执行一次
- “0 0 9 * * *”:每天9点0分0秒执行一次
- “0 0 9 * * MON”:每周一的上午9点整执行
- “0 0 9 1 * *”:每月的第一天上午9点整执行
总之就很强大,可以去了解一下
fixedRate
:是指从每次执行开始到下一次执行开始的固定间隔fixedDelay
:是指一次执行完成到下一次执行开始之间的固定等待时间(会算上执行时间)
那么他们在面对系统休眠而错过下次执行时间的情况,会怎么处理呢?
corn
:当系统唤醒后,cron
调度的任务会在下一个符合表达式的时间点执行。它不会因为错过了预定时间而尝试“补偿”执行fixedRate
:根据实验,系统唤醒后,并不会立即执行任务,而是继续等待,几十分钟后,几乎同时执行了多次任务(休眠中错过的那几次),补偿了失去的过去(可能是按照预计的时间计算了下一次的时间,没有根据实际系统时间校正)1
2
3
4
5
6
7
8
9
10
11
12
13
14#ScheduledTask.Clean 00:44:40.224923500
... //休眠中
#ScheduledTask.Clean 12:25:07.292055800 //11点多唤醒的,过了一会儿才执行
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:25:07.293056400
#ScheduledTask.Clean 12:44:10.973081fixedDelay
:推测,也是继续等待攒满Interval
,但是不会补偿执行,因为间隔是从上一次任务结束后开始计算的
fixedRate
和fixedDelay
的执行机制应该大差不大
让我们从fixedRate
管中窥豹,看看其底层定时机制
定时机制
@Scheduled
可以实现定时任务,这无可厚非,不必质疑,看起来理所当然,理应如此
从来如此,便接受了吗?
为什么呢,你有没有想过,定时,是如何实现的
有两种可能,固定时间点 & 固定时间间隔
第一种,假如我们要在每天的23:00提醒用户睡觉
那么,我们怎么才能在23:00执行特定任务
谁来通知我们,现在已经是23:00了
- 一个简单的想法是:每秒轮询一次时间,如果正好是23时,0分,则执行任务
那么问题就是,任务会被多次触发(60次 maybe)
- 那可以增加一个变量来记录是否执行过,
- 或者把每秒轮询改为每分钟轮询(延迟大),
- 或者检测23时0分0秒(可能错过,定时不一定精准)
- 或者改为每毫秒执行(开销大),
- 或者判断两次执行的间隔中包不包含特定时间点(23:00:00.0000)(最保险)
好的,好的,先别纠结这个了
你刚刚是不是说:每秒轮询一次时间
每秒,轮询,这不又是定时吗,你怎么又绕回来了,自依赖是吧(定时依赖定时)
我们怎么实现每秒轮询?
- 一个简单的想法是:While循环 + Sleep(1000)
好的好的,很完美,就是有点简陋
其实,操作系统提供了定时器,例如linux的cron,Windows的定时任务
硬件时钟和时钟中断(by GPT-4)
- 基础:大多数计算机系统都有一种硬件时钟(通常是实时时钟RTC和高精度事件定时器HPET),它们可以生成中断。这些时钟以固定频率运行,当达到预定时间时,它们会向CPU发送中断信号。
- 工作原理:操作系统内核通过处理这些中断来维持系统时间和调度定时任务。当时钟中断发生时,操作系统的调度器会检查是否有需要执行的任务。
如此依赖,我们便可以实现每秒轮询了,固定点执行任务 GET
对于固定间隔,我们只要根据现在的时间计算出下一次预计执行的时间点即可,完美
小问题
就是有个问题哦,每秒轮询一次貌似有点非常不那么精确
还有一个问题,假如我需要每小时执行一次任务,那么每秒的这些轮询就极大地浪费了CPU
//好吧 其实对CPU来讲是小case啦,但是听着就很不优雅好吧
当然,我们也可以计算出距离时间点的间隔,然后使用系统的定时器,使其一小时后通知我们
@Scheduled定时机制
说了那么多,我们还是来看看@Scheduled
的定时机制是怎么样的
的
的
…
呆——
没错你卡了
经过一下午的搜索和源码阅读(反编译的.class)
我觉得,我们还是抓重点吧,毕竟讲那么细也没什么用,对啊,啊hahaha
> <
借一下大佬的图吧
反正就是
ScheduledAnnotationBeanPostProcessor
会解析@Scheduled
注解,并且把方法(任务)封装为不同类型的Task
实例,缓存在ScheduledTaskRegistrar
中- 其钩子方法
afterSingletonsInstantiated()
在所有单例初始化完成之后回调触发,在此方法中设置了ScheduledTaskRegistrar
中的任务调度器(TaskScheduler
或者ScheduledExecutorService
类型)实例,并且调用ScheduledTaskRegistrar#afterPropertiesSet()
方法添加所有缓存的Task
实例到任务调度器中执行
任务调度器
Scheduling
模块支持TaskScheduler
或者ScheduledExecutorService
类型的任务调度器,而ScheduledExecutorService
其实是java.util.concurrent
的接口,一般实现类就是调度线程池ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
好的好的,其实这里讲了一大堆,我看那源码真的是跳来跳去,云里雾里,还都是反编译的,还都没有索引,还都找不到被谁调用了
好好好
总之呢,任务被放到了一个特殊的数据结构(延迟队列 DelayQueue
)里
内部是一个优先队列,按照预计执行时间排列,头部是最先执行的,take
获取头部时,如果时间还没到,就会休眠等待
不过呢,看源码的话,貌似是自己实现了一个类似的结构,而不是直接用DelayQueue
// 内部的RunnableScheduledFuture
继承了Delayed
1 |
|
是ScheduledThreadPoolExecutor
中的一个静态内部类
来看一下核心的take
方法
1 |
|
采用了Leader-Follower
模式
比较复杂,先不管那个,我们的目的是搞明白为什么系统休眠唤醒后,超时不会立即执行任务
1 |
|
如果第一个任务的时间还没到的话,就会等待,采用Condition
的awaitNanos
方法,进入休眠
而这个方法的实现内部又调用了LockSupport.parkNanos(this, nanos);
1 |
|
最后是调用了Unsafe
的park
方法
这是一个native
方法,从Java层面无法继续往下了,应该是在虚拟机中调用了C++代码,然后进行系统调用
那么,这个Unsafe.park
所等待的时间在Windows上,有可能,在休眠时暂停计时,唤醒时继续计时,休眠时间不计入
这是推测
不过@Scheduled-fixedRate
确实在休眠时暂停了计时,这是实验结果
Conclusion
好的,以上都是乱查查乱看看 + 推测
仅做抛砖引玉,不保证正确性,因为这代码也太绕了,而且还是反编译的!
Peace
Ref
Thread.sleep、synchronized、LockSupport.park的线程阻塞区别 - 知乎 (zhihu.com)
LockSupport中的park与unpark原理_locksupport park-CSDN博客
springBoot中@Scheduled执行原理解析_在springboot中执行定时任务有先后顺序-CSDN博客
通过源码理解Spring中@Scheduled的实现原理并且实现调度任务动态装载 - throwable - 博客园 (cnblogs.com)
Java定时调度机制 - ScheduledExecutorService - 简书 (jianshu.com)