java定时任务目前主要有三种:
- Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行;而且作业类需要集成java.util.TimerTask,一般用的较少。
- Spring3.0以后自带的task,即:spring schedule,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
- Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行;代码稍显复杂。
定时器算法
1.小顶堆
堆,实际上是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左子节点和右子节点的值。
堆又分为两种,最大堆、最小堆。
- 最大堆: 任一非叶子节点的值均大于其左子节点和右子节点的值。根节点的值是最大的。
- 最小堆: 任一非叶子节点的值均小于其左子节点和右子节点的值。根节点的值是最小的。
小顶堆的实现方式
由于堆是一种经过排序的完全二叉树,因此在构建的时候需要对其新插入的节点进行一些操作以使其符合堆的性质。这种操作就叫上浮与下沉。
- 上浮:将当前节点与其父节点相比,如果当前节点的值比父节点小,就把当前节点与父节点交换,然后继续前面的交换,直到当前节点比父节点的值大为止。上浮就是将符合条件的节点往上移的过程。
- 下沉:将当前节点与其左、右子节点相比,如果当前节点的值比其中一个或两个子节点的值大,就把当前节点与两个子节点种比较小的那个交换,,然后继续前面的比较,直到当前节点的值比两个子节点的值都小为止。下沉就是将符合条件的节点往下移的过程。
2.时间轮算法
见名知意,时间轮算法的数据结构类似于钟表上的数据指针,时间轮用环形数组的方式实现,数组中的每个元素都可以称之为槽(和redis集群的槽一样称呼)。槽的内部用双向链表存储着待执行的任务,添加和删除链表的操作时间复杂度为O(1),槽位本身也指代时间精度,比如一秒扫一个槽,那么这个时间轮的最高精度就是1秒。
当有一个延迟任务要插入时间轮时,首先计算其延迟时间与单位时间的余值,从指针指向的当前槽位移动余值的个数槽位,就是该延迟任务需要被放入的槽位。
举个例子,时间轮有8个槽位,编号为 0 ~ 7 。指针当前指向槽位 2 。新增一个延迟时间为 4 秒的延迟任务,4 % 8 = 4,因此该任务会被插入 4 + 2 = 6,也就是槽位6的延迟任务队列。
时间槽位的实现方式
时间轮的槽位实现可以采用循环数组的方式达成,也就是让指针在越过数组的边界后重新回到起始下标。概括来说,可以将时间轮的算法描述为:
用队列来存储延迟任务,同一个队列中的任务,其延迟时间相同。用循环数组的方式来存储元素,数组中的每一个元素都指向一个延迟任务队列。有一个当前指针指向数组中的某一个槽位,每间隔一个单位时间,指针就移动到下一个槽位。被指针指向的槽位的延迟队列,其中的延迟任务全部被触发。在时间轮中新增一个延迟任务,将其延迟时间除以单位时间得到的余值,从当前指针开始,移动余值对应个数的槽位,就是延迟任务被放入的槽位。
基于这样的数据结构,插入一个延迟任务的时间复杂度就下降到 O(1) 。而当指针指向到一个槽位时,该槽位连接的延迟任务队列中的延迟任务全部被触发。
延迟任务的触发和执行不应该影响指针向后移动的时间精确性。因此一般情况下,用于移动指针的线程只负责任务的触发,任务的执行交由其他的线程来完成。比如,可以将槽位上的延迟任务队列放入到额外的线程池中执行,然后在槽位上新建一个空白的新的延迟任务队列用于后续任务的添加。
代码实现
Timer
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("延迟1s执行的任务"+new Date());
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("延迟3s每隔5s执行一次的任务"+new Date());
}
},3000,5000);
}
}
延迟1s执行的任务Sun Oct 10 14:34:13 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:15 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:20 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:25 CST 2021
延迟3s执行的任务Sun Oct 10 14:34:30 CST 2021
Timer的实现方式比较简单,其内部有两个主要的属性:
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
TimerTask是一个实现了Runnable接口的抽象类。其run()方法用于提供具体的延时任务逻辑。
TaskQueue内部采用的是小顶堆的算法实现。根据任务的触发时间采用死循环的方式进行排序,将执行时间最小的任务放在前面。
void add(TimerTask task) {
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
queue[++size] = task;
fixUp(size);
}
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
这样的方式就会有三个问题:
- 由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。
- 如果执行某个任务过程中抛出了异常,那么执行线程将会终止,导致Timer中的其他任务也不能再执行。
- Timer使用的是绝对时间,即是某个时间点,所以它执行依赖系统的时间,如果系统时间修改了的话,将导致任务可能不会被执行。
由于Timer存在上面说的这些缺陷,在JDK1.5中,我们可以使用ScheduledThreadPoolExecutor来代替它,使用Executors.newScheduledThreadPool工厂方法或使用ScheduledThreadPoolExecutor的构造函数来创建定时任务,它是基于线程池的实现,不会存在Timer存在的上述问题,当线程数量为1时,它相当于Timer。
schedule
Spring Schedule在使用前都需要引入spring的包。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
在这里我主要是使用spring boot注解的方式来实现:
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class,args);
}
}
新创建一个类,用来实现定时任务,这个类要注册成为Bean才行。
@Component
public class ScheduleTest {
10 * * * * *:每10秒
* -:区间 示例: 0 0/30 9-17 * * ? : 朝九晚五工作时间内每半小时
*/
@Scheduled(cron="0 * * * * *")
public void doSomething(){
System.out.println("测试schedule的定时器,当秒为0的时候执行一次:"+new Date());
}
}
测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:18:00 CST 2021
测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:19:00 CST 2021
测试schedule的定时器,当秒为0的时候执行一次:Sun Oct 10 19:20:00 CST 2021
@Scheduled注解的另外两个重要属性:fixedRate和fixedDelay
- fixedDelay:上一个任务结束后多久执行下一个任务
- fixedRate:上一个任务的开始到下一个任务开始时间的间隔
@Scheduled(fixedRate = 2000)
public void fixedRate() throws Exception {
System.out.println("fixedRate开始执行时间:" + new Date(System.currentTimeMillis()));
Thread.sleep(1000);
System.out.println("fixedRate执行结束时间:" + new Date(System.currentTimeMillis()));
}
fixedRate开始执行时间:Sun Oct 10 19:59:05 CST 2021
fixedRate执行结束时间:Sun Oct 10 19:59:06 CST 2021
fixedRate开始执行时间:Sun Oct 10 19:59:07 CST 2021
fixedRate执行结束时间:Sun Oct 10 19:59:08 CST 2021
fixedRate开始执行时间:Sun Oct 10 19:59:09 CST 2021
fixedRate执行结束时间:Sun Oct 10 19:59:10 CST 2021
@Scheduled(fixedDelay = 1000)
public void fixedDelay() throws Exception {
System.out.println("fixedDelay开始执行时间:" + new Date(System.currentTimeMillis()));
Thread.sleep(1000 * 2);
System.out.println("fixedDelay执行结束时间:" + new Date(System.currentTimeMillis()));
}
fixedDelay执行结束时间:Sun Oct 10 20:07:23 CST 2021
fixedDelay开始执行时间:Sun Oct 10 20:07:24 CST 2021
fixedDelay执行结束时间:Sun Oct 10 20:07:26 CST 2021
fixedDelay开始执行时间:Sun Oct 10 20:07:27 CST 2021
fixedDelay执行结束时间:Sun Oct 10 20:07:29 CST 2021
如果是强调任务间隔的定时任务,建议使用fixedRate和fixedDelay,如果是强调任务在某时某分某刻执行的定时任务,建议使用cron表达式。
Spring Schedule的Corn是使用的时间轮算法(分层时间轮,每个时间粒度对应一个时间轮,多个时间轮时间进行级联协调)。
在CronSequenceGenerator.java这个类中,对每个CornTask都维护了一下7个Bitset(使用位数组而不用list,set之类的数据结构,一方面是因为空间效率,更重要的是接下来的操作主要是判断某个值是否匹配和从某个值开始找最近的下一个能够匹配的值)
private final BitSet months = new BitSet(12);
private final BitSet daysOfMonth = new BitSet(31);
private final BitSet daysOfWeek = new BitSet(7);
private final BitSet hours = new BitSet(24);
private final BitSet minutes = new BitSet(60);
private final BitSet seconds = new BitSet(60);
```c
然后根据配置的corn值计算这个任务对应的值计算每个bit的值。如我这里配置的每分钟执行一次的CornTask的结果如下:
![image](https:
CronSequenceGenerator负责解析用户配置的Cron表达式,并提供next方法,根据给定的时间获取符合cron表达式规则的最近的下一个时间。CronTrigger实现Trigger的nextExecutionTime方法,根据定时任务执行的上下文环境(最近调度时间和最近完成时间)决定查找下一次执行时间的左边界,之后调用CronSequenceGenerator的next方法从左边界开始找下一次的执行时间。
CronSequenceGenerator的doNext算法从指定时间开始(包括指定时间)查找符合cron表达式规则下一个匹配的时间。如图3-4所示,其整体思路是:沿着秒→分→时→日→月逐步检查指定时间的值。如果所有域上的值都已经符合规则那么指定时间符合cron表达式,算法结束。否则,必然有某个域的值不符合规则,调整该域到下一个符合规则的值(可能调整更高的域),并将较低域的值调整到最小值,然后从秒开始重新检查和调整。
```c
private void doNext(Calendar calendar, int dot) {
List<Integer> resets = new ArrayList<>();
int second = calendar.get(Calendar.SECOND);
List<Integer> emptyList = Collections.emptyList();
int updateSecond = findNext(this.seconds, second, calendar, Calendar.SECOND, Calendar.MINUTE, emptyList);
if (second == updateSecond) {
resets.add(Calendar.SECOND);
}
int minute = calendar.get(Calendar.MINUTE);
int updateMinute = findNext(this.minutes, minute, calendar, Calendar.MINUTE, Calendar.HOUR_OF_DAY, resets);
if (minute == updateMinute) {
resets.add(Calendar.MINUTE);
}
else {
doNext(calendar, dot);
}
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int updateHour = findNext(this.hours, hour, calendar, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_WEEK, resets);
if (hour == updateHour) {
resets.add(Calendar.HOUR_OF_DAY);
}
else {
doNext(calendar, dot);
}
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
int updateDayOfMonth = findNextDay(calendar, this.daysOfMonth, dayOfMonth, this.daysOfWeek, dayOfWeek, resets);
if (dayOfMonth == updateDayOfMonth) {
resets.add(Calendar.DAY_OF_MONTH);
}
else {
doNext(calendar, dot);
}
int month = calendar.get(Calendar.MONTH);
int updateMonth = findNext(this.months, month, calendar, Calendar.MONTH, Calendar.YEAR, resets);
if (month != updateMonth) {
if (calendar.get(Calendar.YEAR) - dot > 4) {
throw new IllegalArgumentException("Invalid cron expression \"" + this.expression +
"\" led to runaway search for next trigger");
}
doNext(calendar, dot);
}
}
private int findNext(BitSet bits, int value, Calendar calendar, int field, int nextField, List<Integer> lowerOrders) {
int nextValue = bits.nextSetBit(value);
if (nextValue == -1) {
calendar.add(nextField, 1);
reset(calendar, Collections.singletonList(field));
nextValue = bits.nextSetBit(0);
}
if (nextValue != value) {
calendar.set(field, nextValue);
reset(calendar, lowerOrders);
}
return nextValue;
}
Quartz
在这里还是使用Spring Boot 集成Quartz;
引入依赖:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
测试的job业务处理类:
public class QuartzJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("执行quartz定时器开始:" + new Date());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行quartz定时器结束:" + new Date());
}
}
实例化Job,将任务触发器加入任务调度中:
@Configuration
public class QuartzConfig {
@Autowired
private Scheduler scheduler;
public void startJob() throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity("job", "group").build();
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1")
.withSchedule(cronScheduleBuilder).build();
scheduler.scheduleJob(jobDetail, cronTrigger);
}
public void deleteJob(String name, String group) throws SchedulerException {
JobKey jobKey = new JobKey(name, group);
if (scheduler.checkExists(jobKey)){
scheduler.deleteJob(jobKey);
}
}
}
测试类(spring容器初始化完成后执行):
@Configuration
public class QuartzTest implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private QuartzConfig quartzConfig;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("容器初始化完成");
try {
quartzConfig.startJob();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
容器初始化完成
2021-10-11 15:02:46.947 INFO 19628 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 7010 (http) with context path ''
2021-10-11 15:02:49.558 INFO 19628 --- [ main] c.c.ScheduleApplication : Started ScheduleApplication in 10.734 seconds (JVM running for 11.644)
执行quartz定时器开始:Mon Oct 11 15:02:50 CST 2021
执行quartz定时器结束:Mon Oct 11 15:02:52 CST 2021
执行quartz定时器开始:Mon Oct 11 15:02:55 CST 2021
执行quartz定时器结束:Mon Oct 11 15:02:57 CST 2021
执行quartz定时器开始:Mon Oct 11 15:03:00 CST 2021
执行quartz定时器结束:Mon Oct 11 15:03:02 CST 2021
|