聊聊定时任务的实现方式

利用JDK实现

Thread Sleep

创建一个线程,把任务放到while循环中,每次执行完任务后sleep指定时间。方法简单,但是能够实现的功能十分有限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SleepTest {
public static void main(String[] args){
final long timeInterval = 1000;
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("你好,同学!");
try {
Thread.sleep(timeInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}

Timer

和Timer相关的类主要有两个:

  • Timer:是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。
  • TimerTask:是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务。

使用方式

Timer的使用也非常简单,需要用到时候new Timer()创建一个定时器工具,然后调用schedule()或者 scheduleAtFixedRate ()方法启动任务。下面是一个简单使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TimerTest {
public static void main(String[] args){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("延迟1秒执行");
}
}, 1000);//只传一个参数delay
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("每隔1秒执行1次");
}
}, 5000, 1000);
timer.schedule((new TimerTask() {
@Override
public void run() {
System.out.println("每隔5秒执行1次");
}
}), 5000, 5000);
}
}

除了上面例子中以固定时间间隔执行任务外,还可以通过调用 scheduleAtFixedRate ()实现以固定频率执行任务。

1
2
3
4
5
6
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("以2秒固定速率执行");
}
},1000, 2000);//延迟1秒执行,每2秒执行一次

固定速率和固定间隔有啥区别呢?也就是schedule() scheduleAtFixedRate ()有啥区别?

  • 相同点

    • 任务执行未超时,下次执行时间 = 上次执行开始时间 + period;
    • 任务执行超时,下次执行时间 = 上次执行结束时间;
  • 不同点

    schedule的策略是错过了就错过了,后续按照新的节奏来走;scheduleAtFixedRate的策略是如果错过了,就努力追上原来的节奏(制定好的节奏)。

    • schedule侧重保持间隔时间的稳定

      schedule方法会因为前一个任务的延迟而导致其后面的定时任务延时。

    • scheduleAtFixedRate保持执行频率的稳定

      如果第n次执行task时,由于某种原因这次执行时间过长,执行完后的systemCurrentTime>= scheduledExecutionTime(第n+1次),则此时不做period间隔等待,立即执行第n+1次task。

底层原理

Timer的原理模型如下图所示:

  1. 其中 TaskQueue 是一个平衡二叉树堆实现的优先级队列,每个 Timer 对象内部有唯一一个 TaskQueue 队列。用户线程调用 timer 的 schedule 方法就是把 TimerTask 任务添加到 TaskQueue 队列,在调用 schedule 的方法时候 long delay 参数用来说明该任务延迟多少时间执行。
  2. TimerThread 是具体执行任务的线程,它从 TaskQueue 队列里面获取优先级最小的任务进行执行,需要注意的是只有执行完了当前的任务才会从队列里面获取下一个任务而不管队列里面是否有已经到了设置的 delay 时间,一个 Timer 只有一个 TimerThread 线程,所以可知 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
public void run() {
try {
mainLoop();
} finally {
// 有人杀死了这个线程,表现得好像Timer已取消
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // 消除过时的引用
}
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
//从队列里面获取任务时候要加锁
synchronized(queue) {
......
}
if (taskFired)
task.run();//执行任务
} catch(InterruptedException e) {
}
}
}

从上述源码可知,当任务执行过程中抛出了除 InterruptedException 之外的异常后,唯一的消费线程就会因为抛出异常而终止,那么队列里面的其他待执行的任务就会被清除。所以 TimerTask 的 run 方法内最好使用 try-catch 结构 catch 主可能的异常,不要把异常抛出到 run 方法外。

总结

Timer使用起来也非常简单,和Thread.sleep()相比,代码实现上更简单,实现的功能也更多,适合单点,执行比较频繁、有规律的场景。

Timer也有很多缺点:

  1. 时间敏感:首先Timer对调度的支持是基于绝对时间的,而不是相对时间,所以它对系统时间的改变非常敏感。
  2. 捕获异常:其次Timer线程是不会捕获异常的,如果TimerTask抛出的了未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,他会错误的认为整个Timer线程都会取消。同时,已经被安排单尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。
  3. 任务冲突:Timer在执行定时任务时只会创建一个线程任务,如果存在多个线程,若其中某个线程因为某种原因而导致线程任务执行时间过长,超过了两个任务的间隔时间,会导致下一个任务执行时间滞后。

这些缺点,在 ScheduledExecutorService 中得到了很好的解决。

ScheduledExecutorService

ScheduledExecutorService是JAVA 1.5后新增的定时任务接口,它是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行。

使用方式

在使用方法上,ScheduledExecutorService和Timer几乎没啥区别,主要通过下面4个方法开启任务:

  • ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit):传入Runnable实例,延迟delay执行一次,单位为unit;
  • <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit):传入Callable实例,延迟delay,执行一次,单位为unit;
  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit):延迟initialDelay,执行周期为period,单位unit;
  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):延迟initialDelay,间隔delay,单位unit。

ScheduledExecutorService接口的默认实现类是ScheduledThreadPoolExecutor,下面举个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ScheduledExecutorServiceTest {
public static void main(String[] args){
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(2);
scheduledThreadPoolExecutor.scheduleAtFixedRate(()->{
System.out.println("This is run by " + Thread.currentThread().getName());
},1,2, TimeUnit.SECONDS);

}
}
//运行结果如下,可以看到任务可能由不同线程执行
This is run by pool-1-thread-1
This is run by pool-1-thread-1
This is run by pool-1-thread-1
This is run by pool-1-thread-2
This is run by pool-1-thread-2

底层原理

和Timer类似,不过Timer中使用的是单个消费线程,ScheduledThreadPoolExecutor中使用了线程池技术。另外ScheduledExecutorService中使用了DelayQueue技术。

总结

ScheduledExecutorService可以理解成Timer的升级版,基于线程池实现,解决了Timer存在的一些问题。在实际应用中,我们应该优先使用ScheduledExecutorService。

Spring中@Schedule注解的使用

从Spring 3开始,Spring自带了一套定时任务工具Spring-Task,可以把它看成是一个轻量级的Quartz,使用起来十分简单,除Spring相关的包外不需要额外的包,支持注解和配置文件两种形式。通常情况下在Spring体系内,针对简单的定时任务,可直接使用Spring提供的功能。

使用方法

在SpringBoot中使用@Schedule非常容易,只需要在启动类上添加@EnableScheduling来开启定时任务即可。

在Spring中使用@Schedule则稍微复杂一点:

  1. 首先需要在配置文件中添加如下配置:

    1
    2
    3
    4
    5
    6
    7
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:task="http://www.springframework.org/schema/task"
    <!-- xsi中添加下面两项-->
    http://www.springframework.org/schema/task
    http://www.springframework.org/schema/task/spring-task-3.1.xsd
    <!-- 在开启注解方式-->
    <task:annotation-driven/>
  2. 给方法加上@Schedule注解即可,它有3中定义方式

    • @Scheduled(fixedDelay = 1000L):固定间隔时间执行任务
    • @Scheduled(fixedRate = 5000):固定频率执行任务,和Timer类似
    • @Scheduled(cron = "0 0 3 * * ?"):cron表达式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //必须要添加@Component,要不然Spring扫描不到
    @Component
    public class ScheduledTasks {
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    //函数必须没有参数,没有返回值
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
    System.out.println(dateFormat.format(new Date()) + "---通过fixedRate定义的定时任务");
    }
    @Scheduled(cron = "0 0 3 * * ?")
    public void job1() {
    System.out.println(dateFormat.format(new Date()) + "---通过cron定义的定时任务");
    }
    @Scheduled(fixedDelay = 1000L)
    public void job2() {
    System.out.println(dateFormat.format(new Date()) + "---通过fixedDelay定义的定时任务");
    }
    }

下表总结了常用的Cron表达式:

表达式 说明
0 0 12 * * ? 每天12点运行
0 15 10 ? * * 每天10:15运行
0 15 10 * * ? 每天10:15运行
0 15 10 * * ? * 每天10:15运行
0 15 10 * * ? 2008 在2008年每天10:15运行
0 * 14 * * ? 每天14点到15点之间每分钟运行一次,开始于14:00,结束于14:59
0 0/5 14 * * ? 每天14点到15点之间每5分钟运行一次,开始于14:00,结束于14:55
0 0/5 14,18 * * ? 每天14点到15点之间每5分钟运行一次,每天18点到19点也每5分钟运行一次
0 0-5 14 * * ? 每天14:10到14:44,每分钟运行一次
0 10,44 14 ? 3 WED 3月每周三的14:10分到14:44,每分钟运行一次
0 15 10 ? * MON-FRI 每周一、二、三、四、五的10:15分运行
0 15 10 15 * ? 每月15日10:15分运行
0 15 10 L * ? 每月最后一天10:15运行
0 15 10 ? * 6L 每月最后一个星期五10:15运行
0 15 10 ? * 6L 2007-2009 在2007/2008/2009年每个月最后一个星期五10:15分运行
0 15 10 ? * 6#3 每月第三个星期五的10:15分运行

实现原理

底层用的也是ScheduledExecutorService,感兴趣的可以自己去了解。

Quartz的使用

Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。另外还提供了集群和持久化的支持。

quartz调度核心元素如下:

  1. Scheduler:任务调度器,是实际执行任务调度的控制器。在spring中通过SchedulerFactoryBean封装起来。
  2. Trigger:触发器,用于定义任务调度的时间规则,有SimpleTrigger,CronTrigger,DateIntervalTrigger和NthIncludedDayTrigger,其中CronTrigger用的比较多,本文主要介绍这种方式。CronTrigger在spring中封装在CronTriggerFactoryBean中。
  3. JobDetail:用来描述Job实现类及其它相关的静态信息,如Job名字、关联监听器等信息。在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用。
  4. Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。实现Job接口的任务,默认是无状态的,若要将Job设置成有状态的,在quartz中是给实现的Job添加@DisallowConcurrentExecution注解(以前是实现StatefulJob接口,现在已被Deprecated),在与spring结合中可以在spring配置文件的job detail中配置concurrent参数。
  5. JobStore: 存储作业和调度期间的状态。

这些元素的关系如下图所示:

Scheduler由SchedulerFactory产生,负责任务的调度。JobDetail可以理解成对Job的一种包装,实际被执行的任务示例是JobDetail,即每次要执行某个job时,会实例化一个新的Job实例放到JobDetail,JobDetail是真正被执行的任务。这样搞的目的主要是提供并发支持。然后,每个Job可以对应一个或多个触发器。

任务的定义

定义一个任务很简单,只需要定义一个实现了Job接口的类QuartzDemo,类中实现执行任务的方法execute()。然后如下所示,创建一个指定名字和组的JobDetail和Trigger,然后把job和trigger放到scheduler中即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class QuartzDemo implements Job{  
@Override
public void execute(JobExecutionContext arg0) throws JobExecutionException {
System.out.println("Quartz执行.......");
}
}
//引进作业程序
JobDetail jobDetail = new JobDetail("jobDetail-s1", "jobDetailGroup-s1", QuartzDemo.class);
//定义一个触发器
SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger", "triggerGroup-s1");
//这里省略触发器的一些配置信息
//作业和触发器设置到调度器中
scheduler.scheduleJob(jobDetail, simpleTrigger);
//启动调度器
scheduler.start();

这样一个任务就定义完成了!

任务的调度

Quartz是基于线程池实现的。 在 Quartz 中,有两类线程,Scheduler 调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。

  • 常规调度线程:调度器实例化后并启动后,调度线程会一直运行, 轮询存储的所有 trigger(调用acquireNextTriggers 默认获取30s内将要触发的所有triggers),如果有需要触发的 trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该 trigger 关联的任务。
  • misfired调度线程: Misfire 线程是扫描所有的 trigger,查看是否有 misfired trigger,如果有的话根据 misfire 的策略分别处理。

任务调度流程图如下:

使用示例

在SpringBoot使用quartz非常简单,在pom中引入spring-boot-starter-quartz依赖可以省去很多准备工作,可以参考这个

下面是Spring中整合Quzrtz的例子:

面用spring整合Quartz,分四步:

  1. 导入依赖
  2. 编写Job
  3. 编写spring配置文件
  4. 启动spring容器(启动调度器)

1. 导入maven依赖

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
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ouhei.quartz</groupId>
<artifactId>ouhei-quartz</artifactId>
<version>1.0.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.6.RELEASE</version>
</dependency>
</dependencies>
</project>

编写Job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.ouhei.quartz;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
* QuartzJobBean实现了Job接口
*
*/
public class MyJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("myJob 执行了............." + context.getTrigger().getKey().getName());
ApplicationContext applicationContext = (ApplicationContext) context.getJobDetail().getJobDataMap()
.get("applicationContext");
System.out.println("获取到的Spring容器是: " + applicationContext);
}
}

配置文件

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
44
45
46
47
48
49
50
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

<!-- 定义任务bean -->
<bean name="myJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<!-- 指定具体的job类 -->
<property name="jobClass" value="org.ouhei.quartz.MyJob" />
<!-- 指定job的名称 -->
<property name="name" value="myJob" />
<!-- 指定job的分组 -->
<property name="group" value="jobs" />
<!-- 必须设置为true,如果为false,当没有活动的触发器与之关联时会在调度器中删除该任务 -->
<property name="durability" value="true"/>
<!-- 指定spring容器的key,如果不设定在job中的jobmap中是获取不到spring容器的 -->
<property name="applicationContextJobDataKey" value="applicationContext"/>
</bean>

<!-- 定义触发器 -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="myJobDetail" />
<!-- 每5秒执行一次 -->
<property name="cronExpression" value="0/5 * * * * ?" />
</bean>

<!-- 定义触发器 -->
<!-- 演示:一个job可以有多个trigger;一个trigger只能有一个job -->
<bean id="cronTrigger2" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="myJobDetail" />
<!-- 每一分钟执行一次 -->
<property name="cronExpression" value="0 */1 * * * ?" />
</bean>

<!-- 定义调度器 -->
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger" />
<ref bean="cronTrigger2" />
</list>
</property>
</bean>

</beans>

启动Spring容器

1
2
3
4
5
6
7
8
9
package org.ouhei.quartz;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("classpath:applicationContext-scheduler.xml");
}
}

参考资料

  1. https://juejin.cn/post/6992719702032121864
  2. https://blog.csdn.net/fuyuwei2015/article/details/83825851
  3. https://segmentfault.com/a/1190000014772752
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信