前言
在实现实验的算法的时候,要想在一个固定间隔时间对代码中某些变量有一个监控或者固定间隔时间处理一些事务逻辑,必不可少的就是定时的任务调度了。同样的,在做项目的过程中,这种定时的任务调度就更加广泛了,比如说间隔一定的时间给用户发送消息,定时清理掉一些数据的任务等等。这边就介绍一下之前用到的三种任务调度方式Timer、ScheduledExecutorService以及Quartz。
一、Timer
1.简单介绍
timer是一个JDK原生的定时工具,其可以根据给定的时间间隔定时调度任务。主要有以下几个核心函数。
1 | schedule(TimerTask task, long delay) |
1.TimerTask是其定时间隔执行的任务,要继承TimerTask并重写run方法。
2.time/firstTime:首次执行任务的时间
3.period:周期性执行Task的时间间隔,单位是毫秒
4.delay:执行task任务前的延时时间,单位是毫秒
这边再简单说一下schedule和scheduleAtFixedRate两个函数的区别:scheduleAtFixedRate每次执行时间为上一个任务开始起向后推一个period时间间隔。也就是下一次开始的时间是上一次任务开始的时间点加上间隔时间,因此不会存在延后开始执行的情况,但是有可能会存在并发执行的问题,因为上一个可能在下一个任务开始的时候上一个任务还没有结束。schedule每次执行时间为上一个任务结束之后向后推一个peroid时间,若上一个任务结束的晚,那么就会存在延后的情况。若上一个任务结束的早,那么就会存在提前开始的情况。
2.简单使用
比如我这边要实现的功能是:实现求和并按照时间间隔监控此时的sum是多少。
1 | public class testTimer { |
使用其实非常简单,只要定义好定时执行的类CalcuTask并将其继承于TimerTask,然后根据需求判断调用scheduleAtFixedRate函数还是schedule函数即可。主要来看下源码是怎么实现的。
3.源码实现
在没有看源码实现之前,我能想到的实现方式会是这样:Timer也就是一个线程每隔一定的时间去监控另外一个线程的状态,也就是每隔一定的时间去起一个线程来执行监控。看一下Timer的实现方式
主要有四个类
1 | Timer : 创建定时任务调度的主类 |
看过源码后发现,其实Timer实现背后是基于一个单线程,所有任务都是基于同一个线程来调度的和串行执行,当监控任务的执行中比较复杂的时候就会影响到下一次的监控,造成延迟。
在Timer类中创建一个TimerThread对象和TaskQueue对象,其中TaskQueue中可以存放多个TimerTask,本质就是一个TimerTask类型的优先级队列,注解中写的是balanced binary heap,也可以理解为一个小顶堆,是以nextExecutionTime参数进行排序的,每次出队列的是第一个TimerTask,也就是当前nextExecutionTime最小的一个定时任务。
Timer类中的两个参数,第一个参数是一个队列的类,队列中存放的是类型是TimerTask的实例。第二个参数是一个线程,里面存放的是TaskQueue实例。
1 | /** |
四个构造函数。可以看成只有两个 ,构造函数中设置计时器的名字,设置是否为守护线程。
1 | public Timer() { |
六种定时方法及其解释。
1 | //调用后延迟delay秒执行一次。 |
sched方法。主要就是将当前task放入队列中,执行的过程中对其加锁,防止其他线程的影响。
1 | private void sched(TimerTask task, long time, long period) { |
其中queue的add方法是这样的。
1 | void add(TimerTask task) { |
下面来看一下TimerThread类的run方法是如何执行的。
1 | public void run() { |
关键的mainLoop函数。
1 | /** |
Timer的源码分析到这里基本就完了,总结一下:第一,Timer是一个单线程的定时器,从大方面来说主要能解决两种定时的问题,一种是到某个时间执行一次或者按照间隔执行定时任务,另一种是调用后延迟delay秒开始执行,这个延迟可以自己设定。第二,schedule函数和scheduleAtFixedRate函数的区别。第三,由于Timer是基于单线程的,当定时任务很复杂的时候,就不太适合用这个来做了,容易造成任务的延迟。这时候就需要用到ScheduledExecutorService这种线程池的方式来进行定时的任务调度。
二、ScheduledExecutorService
1.简单介绍
ScheduledExecutorService能克服Timer的不足,因为ScheduledExecutorService是基于线程池的,所以能支持多个线程并发执行,之间不受影响。至于原理是什么,可以看我的这篇博客 线程与线程池。
ScheduledExecutorService主要有四个函数
1 | //这个方法将在给定的延迟时间后执行callable。 |
这边就用ScheduledExecutorService简单的实现上面Timer一样的功能。
2.简单使用
1 | public class testScheduledExecutorService { |
三、Quartz
1.简单介绍
虽然ScheduledExecutorService对Timer进行了线程池的改进,但是依然无法满足复杂的定时任务调度场景。因此OpenSymphony提供了强大的开源任务调度框架:Quartz。Quartz是纯Java实现,而且作为Spring的默认调度框架,由于Quartz的强大的调度功能、灵活的使用方式、还具有分布式集群能力,可以说Quartz出马,可以搞定一切定时任务调度。比方说火车订票之后超过三十分钟未支付为判断为无效,邮件的定时发送等等场景。
Quartz主要由三个基础部分组成
- 调度器:Scheduler
- 任务:JobDetail
- 触发器:Trigger,包括SimpleTrigger和CronTrigger
其中任务就是最基本的要实现的部分,比方说上述的定时发送邮件,定时监控有没有支付成功等等。其次,需要有一个触发器去触发执行,指定运行的时间、执行的间隔和执行的次数等等。最后用调度器将任务和触发器结合起来,做到用指定的触发器去触发指定的任务。
2.简单使用
比如我要实现一个定时一秒钟输出一句简单的内容,其中这句话是由外部传入Job的,可以这么实现。
定时的实现类testQuartz
1 | package test.Quartz; |
Job类MyJob
1 | package test.Quartz; |
在控制台就能每隔一秒钟输出一次内容。也就是每次都是新的一个Job去执行这个任务,那么现在如果我要一秒钟对一个int值累加1该如何实现,在Job中给我们提供了@PersistJobDataAfterExecution,就能将上一次的结果拿到下一次来用,并且为了避免并发可能造成数据混乱的错误,还应该加上@DisallowConcurrentExecution。
Job类改为如下
1 | package test.Quartz; |