java定时任务目前主要有三种:

  1. Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行;而且作业类需要集成java.util.TimerTask,一般用的较少。
  2. Spring3.0以后自带的task,即:spring schedule,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
  3. Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行;代码稍显复杂。

定时器算法

1.小顶堆

堆,实际上是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左子节点和右子节点的值。

堆又分为两种,最大堆、最小堆。

- 最大堆: 任一非叶子节点的值均大于其左子节点和右子节点的值。根节点的值是最大的。

- 最小堆: 任一非叶子节点的值均小于其左子节点和右子节点的值。根节点的值是最小的。

小顶堆的实现方式

由于堆是一种经过排序的完全二叉树,因此在构建的时候需要对其新插入的节点进行一些操作以使其符合堆的性质。这种操作就叫上浮与下沉。

  • 上浮:将当前节点与其父节点相比,如果当前节点的值比父节点小,就把当前节点与父节点交换,然后继续前面的交换,直到当前节点比父节点的值大为止。上浮就是将符合条件的节点往上移的过程。
  • 下沉:将当前节点与其左、右子节点相比,如果当前节点的值比其中一个或两个子节点的值大,就把当前节点与两个子节点种比较小的那个交换,,然后继续前面的比较,直到当前节点的值比两个子节点的值都小为止。下沉就是将符合条件的节点往下移的过程。

2.时间轮算法

见名知意,时间轮算法的数据结构类似于钟表上的数据指针,时间轮用环形数组的方式实现,数组中的每个元素都可以称之为槽(和redis集群的槽一样称呼)。槽的内部用双向链表存储着待执行的任务,添加和删除链表的操作时间复杂度为O(1),槽位本身也指代时间精度,比如一秒扫一个槽,那么这个时间轮的最高精度就是1秒。

当有一个延迟任务要插入时间轮时,首先计算其延迟时间与单位时间的余值,从指针指向的当前槽位移动余值的个数槽位,就是该延迟任务需要被放入的槽位。

举个例子,时间轮有8个槽位,编号为 0 ~ 7 。指针当前指向槽位 2 。新增一个延迟时间为 4 秒的延迟任务,4 % 8 = 4,因此该任务会被插入 4 + 2 = 6,也就是槽位6的延迟任务队列。

时间槽位的实现方式

时间轮的槽位实现可以采用循环数组的方式达成,也就是让指针在越过数组的边界后重新回到起始下标。概括来说,可以将时间轮的算法描述为:

用队列来存储延迟任务,同一个队列中的任务,其延迟时间相同。用循环数组的方式来存储元素,数组中的每一个元素都指向一个延迟任务队列。有一个当前指针指向数组中的某一个槽位,每间隔一个单位时间,指针就移动到下一个槽位。被指针指向的槽位的延迟队列,其中的延迟任务全部被触发。在时间轮中新增一个延迟任务,将其延迟时间除以单位时间得到的余值,从当前指针开始,移动余值对应个数的槽位,就是延迟任务被放入的槽位。

基于这样的数据结构,插入一个延迟任务的时间复杂度就下降到 O(1) 。而当指针指向到一个槽位时,该槽位连接的延迟任务队列中的延迟任务全部被触发。

延迟任务的触发和执行不应该影响指针向后移动的时间精确性。因此一般情况下,用于移动指针的线程只负责任务的触发,任务的执行交由其他的线程来完成。比如,可以将槽位上的延迟任务队列放入到额外的线程池中执行,然后在槽位上新建一个空白的新的延迟任务队列用于后续任务的添加。

代码实现

Timer

/**
* @className: TimerTest
* @description: 测试java.util.Timer的定时器实现
* @author: charon
* @create: 2021-10-10 10:35
*/
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
// 延迟1s执行任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("延迟1s执行的任务"+new Date());
}
},1000);
// 延迟3s执行任务,每隔5s执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("延迟3s每隔5s执行一次的任务"+new Date());
}
},3000,5000);
// try {
// Thread.sleep(5000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// timer.cancel();
// System.out.println("任务执行完毕"+new Date());
}
} 延迟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的实现方式比较简单,其内部有两个主要的属性:

/**
* 用于存放定时任务TimeTask的列表
*/
private final TaskQueue queue = new TaskQueue();
/**
* 用于执行定时任务的线程
*/
private final TimerThread thread = new TimerThread(queue);

TimerTask是一个实现了Runnable接口的抽象类。其run()方法用于提供具体的延时任务逻辑。

TaskQueue内部采用的是小顶堆的算法实现。根据任务的触发时间采用死循环的方式进行排序,将执行时间最小的任务放在前面。

void add(TimerTask task) {
// Grow backing store if necessary
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. 由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。
  2. 如果执行某个任务过程中抛出了异常,那么执行线程将会终止,导致Timer中的其他任务也不能再执行。
  3. 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注解的方式来实现:

/**
* 在spring boot的启动类上面添加 @EnableScheduling 注解
*/
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class,args);
}
}

新创建一个类,用来实现定时任务,这个类要注册成为Bean才行。

/**
* @className: ScheduleTest
* @description: 测试schedule的执行器
* @author: charon
* @create: 2021-10-10 19:04
*/
@Component
public class ScheduleTest {
/**
* corn表达式:秒、分、时、日、月、星期
* 值可以是数字,也可以是以下符号:
* *:所有值都匹配 示例: 0 0 * * * *:每小时(当秒和分都为0的时候)
* ?:只能用在日期和星期两个表达式种 示例: 0 0 12 * * ? 每天中午12点触发
* ,:或者 示例:0 0 9,13 * * *:每天的9点和13点
* /:增量值 示例: */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:上一个任务的开始到下一个任务开始时间的间隔
/**
* 测试fixedRate,每2s执行一次
* @throws Exception
*/
@Scheduled(fixedRate = 2000)
public void fixedRate() throws Exception {
System.out.println("fixedRate开始执行时间:" + new Date(System.currentTimeMillis()));
//休眠1秒
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 /**
* 等上一次执行完等待1s执行
* @throws Exception
*/
@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);

然后根据配置的corn值计算这个任务对应的值计算每个bit的值。如我这里配置的每分钟执行一次的CornTask的结果如下:

CronSequenceGenerator负责解析用户配置的Cron表达式,并提供next方法,根据给定的时间获取符合cron表达式规则的最近的下一个时间。CronTrigger实现Trigger的nextExecutionTime方法,根据定时任务执行的上下文环境(最近调度时间和最近完成时间)决定查找下一次执行时间的左边界,之后调用CronSequenceGenerator的next方法从左边界开始找下一次的执行时间。

CronSequenceGenerator的doNext算法从指定时间开始(包括指定时间)查找符合cron表达式规则下一个匹配的时间。如图3-4所示,其整体思路是:沿着秒→分→时→日→月逐步检查指定时间的值。如果所有域上的值都已经符合规则那么指定时间符合cron表达式,算法结束。否则,必然有某个域的值不符合规则,调整该域到下一个符合规则的值(可能调整更高的域),并将较低域的值调整到最小值,然后从秒开始重新检查和调整。

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);
// 下一个匹配值是-1,则将对更高的域做加1操作,从0开始查找下一个匹配值,将当前域设置为下一个匹配值,重置比当前域低的所有域设置为最小值,递归调度本算法。
if (nextValue == -1) {
calendar.add(nextField, 1);
reset(calendar, Collections.singletonList(field));
nextValue = bits.nextSetBit(0);
}
// 下一个匹配值不是当前值但也不是-1,则将当前域设置为下一个匹配值,将比当前域低的所有域设置为最小值,递归调度本算法
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业务处理类:

/**
* @className: QuartzJob
* @description: 业务逻辑处理类
* @author: charon
* @create: 2021-10-11 14:32
*/
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,将任务触发器加入任务调度中:

/**
* @className: QuartzConfig
* @description: scheduler的启动、结束等控制类
* @author: charon
* @create: 2021-10-11 14:38
*/
@Configuration
public class QuartzConfig { @Autowired
private Scheduler scheduler; /**
* 开始定时器
*/
public void startJob() throws SchedulerException {
// 通过JobBuilder构建JobDetail实例,JobDetail规定只能是实现Job接口的实例
// JobDetail 是具体Job实例
JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity("job", "group").build();
// 基于表达式构建触发器 每5秒种执行一次
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
// CronTrigger表达式触发器 继承于Trigger
// TriggerBuilder 用于构建触发器实例
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1")
.withSchedule(cronScheduleBuilder).build();
scheduler.scheduleJob(jobDetail, cronTrigger);
} /**
* 删除某个任务
*
* @param name job的名称
* @param group job的分组
* @throws SchedulerException
*/
public void deleteJob(String name, String group) throws SchedulerException {
JobKey jobKey = new JobKey(name, group);
if (scheduler.checkExists(jobKey)){
scheduler.deleteJob(jobKey);
}
}
}

测试类(spring容器初始化完成后执行):

/**
* @className: QuartzTest
* @description: 测试quartz的定时器
* @author: charon
* @create: 2021-10-11 10:33
*/
@Configuration
public class QuartzTest implements ApplicationListener<ContextRefreshedEvent> { @Autowired
private QuartzConfig quartzConfig; /**
* 监听初始化quartz
* @param event
*/
@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

参考文章:

https://blog.csdn.net/liuxiao723846/article/details/90546619

https://www.cnblogs.com/WangJinYang/p/15160354.html

https://blog.csdn.net/andy2019/article/details/79436869

https://blog.csdn.net/weixin_42522400/article/details/112783846

java定时任务调度框架的更多相关文章

  1. 项目一:第十四天 1.在realm中动态授权 2.Shiro整合ehcache 缓存realm中授权信息 3.动态展示菜单数据 4.Quartz定时任务调度框架—Spring整合javamail发送邮件 5.基于poi实现分区导出

    1 Shiro整合ehCache缓存授权信息 当需要进行权限校验时候:四种方式url拦截.注解.页面标签.代码级别,当需要验证权限会调用realm中的授权方法   Shiro框架内部整合好缓存管理器, ...

  2. [源码分析] 定时任务调度框架 Quartz 之 故障切换

    [源码分析] 定时任务调度框架 Quartz 之 故障切换 目录 [源码分析] 定时任务调度框架 Quartz 之 故障切换 0x00 摘要 0x01 基础概念 1.1 分布式 1.1.1 功能方面 ...

  3. 通过源码分析Java开源任务调度框架Quartz的主要流程

    通过源码分析Java开源任务调度框架Quartz的主要流程 从使用效果.调用链路跟踪.E-R图.循环调度逻辑几个方面分析Quartz. github项目地址: https://github.com/t ...

  4. Java定时任务调度详解

    前言 在实际项目开发中,除了Web应用.SOA服务外,还有一类不可缺少的,那就是定时任务调度.定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券 ...

  5. 一文揭秘定时任务调度框架quartz

    之前写过quartz或者引用过quartz的一些文章,有很多人给我发消息问quartz的相关问题, quartz 报错:java.lang.classNotFoundException quartz源 ...

  6. java定时任务调度工具

    一.什么是定时任务调度 基于给定的时间点,给定的时间间隔或者给定的时间执行次数自动执行的任务. 二.java中常用的定时任务调度工具: Timer Quartz 2.1两者区别: Timer源自jdk ...

  7. Java——定时任务调度工具

    一.什么是定时任务调度? 1.常用的定时调度工具:Timer和Quartz 二.Timer简介 1.Timer的定义以及架构 2.Timer示例 三.Timer的定时调度函数 1.schedule的四 ...

  8. JAVA定时任务调度之Timer入门详解(一)

    所谓的Timer,打开jdk的api文档可以看到它的定义:一种工具,线程用其安排以后在后台线程中执行的任务.可安排任务执行一次,或者定期重复执行.通俗点讲就是说:有且仅有一个后台线程对多个业务线程进行 ...

  9. java定时任务调度-Timer(1)

    一.定义 有且仅有一个后台线程对多个业务线程进行定时定频率的调度 二. Timer  ---->  Timer Task  (中有run();方法) 通过 new Timer().schedul ...

随机推荐

  1. 云原生学习筑基 ~ 组网必备知识点 ~ DNS服务

    @ 目录 一.为啥写这篇文章? 二.DNS的作用 三.域 四.DNS工作原理 五.搭建DNS服务器 5.1.Bind 5.2.系统环境准备 5.3.安装 5.4.查看bind的相关文件 5.5.查看b ...

  2. 判断页面是在pc端还是移动端打开不同的页面

    在pc端页面上的判断 var mobileAgent = new Array("iphone", "ipod", "ipad", " ...

  3. python生成时间序列(date_range)

    介绍 自己写了一个用python内置模块实现的生成时间序列的函数 支持自动推断字符串到datetime的转换, 但对格式有一定要求, 其它格式可手动指定格式化方式, 格式化方式与python内置格式化 ...

  4. (7)java Spring Cloud+Spring boot+mybatis企业快速开发架构之SpringCloud-Spring Boot Starter的介绍及使用

    ​ Spring Boot 的便利性体现在,它简化了很多烦琐的配置,这对于开发人员来说是一个福音,通过引入各种 Spring Boot Starter 包可以快速搭建出一个项目的脚手架推荐分布式架构源 ...

  5. ✔PHP文件包含漏洞全面总结

    我的另一篇博客总结的不够全面,但依然有借鉴价值:https://www.cnblogs.com/Zeker62/p/15192610.html 目录 文件包含的定义 文件包含漏洞常见函数 文件包含漏洞 ...

  6. Java学习之随堂笔记系列——day04

    今日内容1.break和continue关键字以及循环嵌套    1.1 break和continue的区别?        continue表示跳过当前循环,继续执行下一次循环break表示结束整个 ...

  7. 微信小程序自动化测试

    使用官方工具 使用webview测试方法,当2019年被微信封禁 使用native定位

  8. JMeter主要元件

    配置元件 http cookie管理器 http信息头管理器 http请求默认值 统一管理 快速切换测试环境 http cache管理器 静态资源 监听器元件 查看结果树 分析查看某个请求的详情 请求 ...

  9. vue three.js 结合tween.js 实现动画过渡

    参考地址:https://www.jianshu.com/p/d6e3b4b153bb https://www.jqhtml.com/10513.html 官方文档:https://github.co ...

  10. P7737-[NOI2021]庆典【tarjan,虚树】

    正题 题目链接:https://www.luogu.com.cn/problem/P7737 题目大意 给出一张无向图满足若\(x\Rightarrow z,y\Rightarrow z\)那么有\( ...