作业调度 Quartz

前言

早期刚接触到消息中间件的时候,我反问自己:消息中间件不就是个FIFO嘛!为什么还要实现这么复杂的RMQ或者Kafka。当然,如果你要抛去中间件的高可用与高性能去看待,无可厚非,它就是一个FIFO。但是学习中间件不就是去学习大佬们为了提高性能、可用的一些手段嘛!拿RMQ来说,整个精华就在与它的内存映射、零拷贝以及顺序写代替随机写等(个人理解)。

同样的看到作业调度Quartz,我也在想:这个不是就是Java中的Timer吗?所以后面我就来探究一下Quartz与Timer有什么区别,借此学习一下Quartz的精华

Timer

Java中TImer可以帮助我们实现一些简单的定时任务,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String args[]){
Timer timer = new Timer();
TaskTimer taskTimer = new TaskTimer();
timer.schedule(taskTimer,new Date(),1000);
//taskTimer.run();
}

public static class TaskTimer extends TimerTask{
public void run() {
System.out.println("Timer Task Do Something");
}
}
}

对于简单的而且实时性不高的任务,可以采用Timer来完成。但是该类在API中也说明了,不能保证触发的时间是正确的。 因为Timer底层是使用一个单线来实现多个Timer任务处理的,所有任务都是由同一个线程来调度,所有任务都是串行执行,意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。而且一个任务如果抛出了异常,会导致后续的任务不能继续执行。

ScheduledExecutorService

Timer的缺陷可以使用ScheduledExecutorService来替代。ScheduledExecutorService是基于线程池的,可以开启多个线程进行执行多个任务,每个任务开启一个线程,这样就可以避免上述的两个致命缺陷。

1
2
3
4
5
6
7
8
9
//创建并执行延时任务
schedule(Callable<V> callable, long delay, TimeUnit unit)
schedule(Runnable command, long delay, TimeUnit unit)

//循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnitunit)

//循环任务,以上一次任务的结束时间计算下一次任务的开始时间
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit)

但是更复杂的一些定时任务ScheduledExecutorService却实现不了,或者说实现起来很困难。比如每个月的月末发送工资,这里就需要考虑2.28、大小月、闰年等因素,或者节假日发放补贴任务对于ScheduledExecutorService都是比较难实现的。

Quartz

Quartz的某次执行任务过程中抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务;而TimerTask则不同,一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务。可以看一下Quartz的简单使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

/**
* Created by 64669 on 2019/4/17.
*/
public class MainScheduler {

//创建调度器
public static Scheduler getScheduler() throws SchedulerException{
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
return schedulerFactory.getScheduler();
}


public static void schedulerJob() throws SchedulerException{
//创建任务
JobDetail jobDetail = JobBuilder.newJob(MyJob.class).withIdentity("Test Job", "Test Job Group").build();

//创建触发器 每3秒钟执行一次
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("Test Trigger", "Test Trigger Group")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever())
.build();

//创建调度器
Scheduler scheduler = getScheduler();
//将任务及其触发器放入调度器
scheduler.scheduleJob(jobDetail, trigger);
//调度器开始调度任务
scheduler.start();

}

public static void main(String[] args) throws SchedulerException {
MainScheduler mainScheduler = new MainScheduler();
mainScheduler.schedulerJob();
}
}

Quartz的精华

目前还没有发现Quartz有哪些精华- - ,就先占个坑把。等后面接触深了再来补这块。

定时任务的底层原理

在看Timer源码之前,我先问一下自己:要让你自己实现一个定时任务功能你会怎么实现?第一想法就是开启一个线程一直轮询看当前时间是否已经到了任务执行的时间了。这样缺点非常的明显,轮询会造成CPU的浪费。所以来看看Timer是如何实现的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die

// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
//如果任务的执行时间还没到,就计算出还有多久才到达执行时间,然后线程进入休眠
if (!taskFired)
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}

如果懒得看上面的代码,可以直接定位到下面代码:

1
2
3
//如果任务的执行时间还没到,就计算出还有多久才到达执行时间,然后线程进入休眠
if (!taskFired)
queue.wait(executionTime - currentTime);

它具体的实现是通过Timer内部自定义的一个队列TaskQueue ,TaskQueue是一个小顶堆,一次执行时间距离现在最小的会被放在堆顶,到时执行线程直接获取堆顶任务并判断是否执行即可。而且它并不是采用轮询的方式去判断堆顶的任务是否已经到达执行的时间点了,而是通过 queue.wait (下次执行的时间 - 当前时间)让线程阻塞了一段时间

参考文章

定时器的几种实现方式

Quartz-Trigger详解