1. 不浮于表面的小红
上次出色地完成了老板给的重要任务,小红已经升职加薪了。作为资深的搬砖大师,岂能仅仅满足实现出来。这不,小红正在看“晦涩的”源码,准备对定时任务的机制一探究竟。
2. 假设自己来实现定时任务调度
小红心想还好Java对定时任务有内置的支持,要是Java没有提供Timer
和ScheduledExecutorService
,要实现定时任务调度怎么办呢? 小红想了下,如果要实现定时任务的调度,至少需要如下特性:
- 任务接收 这个简单能想到的就是用一个队列来接收任务,然后执行任务的线程从队列中获取。
- 任务需尽可能在规定的时间延迟执行 一直循环检测是否当前有任务需要立刻执行。那如果没有任务的时候怎么办?会不会引起CPU空转,如果要增加休眠,那休眠时间多长合适? 如果使用线程阻塞,那何时唤醒?阻塞是否一直等待,而不用超时?
- 总是时间点靠前的任务先执行,哪怕这个任务后于其他任务添加 如果有多个任务,是否需要依次检查所有的任务,看是否有需要当前执行的? 还是说,将任务按照先后执行顺序进行排序,每次就检测第一个?
还别说,要实现个定时任务调度,要考虑的地方还真多。接下来我们看看前面用的Timer
是怎么解决这些问题的。
3. Timer
1. 任务接收
Timer
内部维护了一个TaskQueue
用于接收任务。TaskQueue
内部是一个数组(默认大小为128),会在任务数据超过128时,进行扩容,每次扩容后为扩容前的两倍。
2. 任务需尽可能在规定的时间延迟执行
Timer
内部维护了一个TimerThread
,该线程会在构造Timer
的时候开启。开启后,会一直扫描上述的TaskQueue
是否有任务可以执行。 那么什么样的任务才是可执行的? 调度时间在当前时间或之前(调度时间在当前时间之前的场景,我们在第一篇中已经有涉及,前续任务耗时影响了后续任务),那么就是可执行的。
- 假设我们添加了一个当前时间的任务,
TimerThread
就会扫描到任务,并直接执行。 - 那如果我们添加了一个明天才执行的任务,
TimerThread
如何扫描呢?
聪明的TimerThread
这时已经知道了这个任务要明天才会执行,所以才不会傻乎乎的一直去看检测任务是否可以执行。而是直接阻塞到这个任务需要执行的时间点。这样就避免了CPU空转,也能及时地在该执行任务的时候及时醒来。Timer
使用如下代码实现该阻塞机制。
if (!taskFired) // 任务还没有到执行的时机
queue.wait(executionTime - currentTime);
3. 总是时间点靠前的任务先执行,哪怕这个任务后于其他任务添加
到这里,我们终于知道如何在任务该执行的时候执行任务了,并且避免了CPU空转这样的资源浪费。那么这时还有一个问题: 我们假设当前时间为10:00 am,任务task1
的调度时间为11:00 am,task2
的调度时间为10:05 am。并且我们在10点告诉Timer
,我们有一个task1
需要定时调度;然后我们忙活其他事情去了,接着在10:02am,我们告诉Timer
还有一个task2
需要定时调度。
按照我们前面了解到的第一和第二点,流程应该是这样的:
- 10:00 am, 添加定时调度
task1
。 - 接着
TimerThread
会扫描到任务task1
,然后发现要11:00 am才执行,所以TimerThread
就设置了个11:00 am的闹钟,然后就去玩吃鸡去了。 - 10:02 am,我们又添加了一个定时调度任务
task2
。 - 但是这时,
TimerThread
才刚开始玩吃鸡呢,一直玩到11:00 am,所以task2
就得等到TimerThread
吃鸡游戏结束后,并把task1
执行完了,才能被执行了。至少也要等到11:00 am之后去了。
这怎么行,正事都不做,就去玩吃鸡去了?于是我们准备去问责TimerThread
。 正在吃鸡的TimerThread
表示,啥,有活干?我不知道啊,我闹钟到11:00 am,那时才有活干。
为了进一步压榨TimerThread
,让TimerThread
只要有活的时候就立马干活。Timer
增加了一个温馨的“唤醒服务”。只要在有更紧急(时间更靠前)的任务,就去唤醒TimerThread
从吃鸡游戏中回到任务调度上来。 这里我们不得不先将最开始提到的TaskQueue
拉出来再遛遛,除了前面涉及的数据结构和扩容机制。还有一个很重要的特性,那就是按照任务的执行时间先后顺序进行排序。也即越先执行的任务,就会从TaskQueue
越先扫描出来。
那么我们来看下Timer
是如何进一步压榨TimerThread
的:
- 10:00 am, 添加定时调度
task1
。 - 接着
TimerThread
会扫描到任务task1
,然后发现要11:00 am才执行,所以TimerThread
就设置了个11:00 am的闹钟,然后就去玩吃鸡去了。 - 10:02 am,我们又添加了一个定时调度任务
task2
。 - 那么此时
TimerThread
中就包含两个任务:[task1
,task2
]。并会对任务按照执行时间进行排序,排序后 [task2
,'task1']。 Timer
会比较新添加的任务是否和排序后的最早需要执行的任务是同一个任务。如果是同一个任务,则表明这个任务更紧急,需要将TimerThread
叫过来干活了;如果不是同一个任务,那么就悄悄把任务放到队列就好了,因为TimerThread
前面已经知道什么时候应该过来执行最近的一个任务。
“唤醒服务”其实很简单,使用notify()
即可。
if (queue.getMin() == task)
queue.notify();
4. 真相只有一个
小红终于轻车熟路地从“晦涩的”源码中,对Timer
定时调度机制了解得一清二楚。不就是几个线程阻塞唤醒、一个小排序什么什么的吗?并还画出了Timer运作机制的简易示意图:
小红此时已经不惧怕是否有定时任务工具类了,甚至还能定制化独特的“小红牌”定时调度呢。不愧为资深搬砖大师,小红端着泡着蒙顶山茶的水杯,来到窗边,世界如此简单 !
本文首发于公众号:「90后技术宅」,一线码农的成长点滴分享。