我们先来一个谜题,一起猜一猜。
谜题:
小马不停蹄,日夜不休息,一阵铃儿响,催人争朝夕。 (打一常用物)文章末尾揭晓谜底。
定时任务的适用场景
定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券;比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。
定时任务实现的四种方式
JDK定时类、Quartz 框架、Spring注解定时和xml配置、SpringBoot定时任务。
1、JDK定时
Timer是一种定时器工具,用来在一个后台线程计划执行指定任务。它可以计划执行一个任务一次或反复多次。
TimerTask一个抽象类,它的子类代表一个可以被Timer计划的任务。具体的任务在TimerTask中run接口中实现。
通过Timer中的schedule方法启动定时任务。
01、在指定日期运行定时器任务,只运行一次
public static void main(String[] args) throws ParseException {
String sdate = "2018-02-14";
SimpleDateFormat sf = new SimpleDateFormat("yy-MM-dd");
Date date = sf.parse(sdate);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("系统正在运行……");
}
}, date); //在指定的日期运行一次定时任务
/*如果date日期在今天之前,则启动定时器后,立即运行一次定时任务run方法*/
/*如果date日期在今天之后,则启动定时器后,会在指定的将来日期运行一次任务run方法*/
}
02、在距当前时刻的一段时间后运行定时器任务,只运行一次
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("系统正在运行……");
}
}, 5000); //指定启动定时器5s之后运行定时器任务run方法,并且只运行一次
}
03、在指定的时间后,每隔指定的时间,重复运行定时器任务
public static void main(String[] args) throws ParseException {
String sdate = "2018-02-10";
SimpleDateFormat sf = new SimpleDateFormat("yy-MM-dd");
Date date = sf.parse(sdate);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("系统正在运行……");
}
}, date, 2000);
/*如果指定的date时间是当天或者今天之前,启动定时器后会立即每隔2s运行一次定时器任务*/
/*如果指定的date时间是未来的某天,启动定时器后会在未来的那天开始,每隔2s执行一次定时器任务*/
}
04、在距当前时刻的一段指定距离后,每隔指定时间运行一次定时器任务
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("系统正在运行……");
}
}, 5000, 2000);
/*当启动定时器后,5s之后开始每隔2s执行一次定时器任务*/
}
停止定时器的四种方式
1、调用Timer的cancel方法;
2、把Timer线程设置成Daemon守护线程,当所有的用户线程结束后,那么守护线程也会被终止;
3、当所有的任务执行结束后,删除对应Timer对象的引用,线程也会被终止;
4、调用System.exit方法终止程序
2、Quartz
Quartz是一个完全由Java编写的开源作业调度框架,为在Java应用程序中进行作业调度提供了简单却强大的机制。
pom文件
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
实现job接口在execute方法写相关业务
public class TestJob implements Job {
private Logger logger = LoggerFactory.getLogger(TestJob.class);
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("业务写在这里");
logger.info(Thread.currentThread().getName() + " test job begin " + new Date());
}
}
测试类
public static void main(String[] args) throws InterruptedException, SchedulerException {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
// 开始
scheduler.start();
// job 唯一标识 test.test-1
JobKey jobKey = new JobKey("test" , "test-1");
JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity(jobKey).build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("test" , "test")
// 延迟一秒执行
.startAt(new Date(System.currentTimeMillis() + 1000))
// 每隔一秒执行 并一直重复
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever())
.build();
scheduler.scheduleJob(jobDetail , trigger);
Thread.sleep(5000);
// 删除job
scheduler.deleteJob(jobKey);
}
public static void main(String[] args) throws InterruptedException, SchedulerException {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
// 开始
scheduler.start();
// job 唯一标识 test.test-1
JobKey jobKey = new JobKey("test" , "test-1");
JobDetail jobDetail = JobBuilder.newJob(TestJob.class).withIdentity(jobKey).build();
// 可以设置定时任务具体时间
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("test", "test-1").withSchedule(
CronScheduleBuilder.cronSchedule("/2 * * * * ?")
).build();
scheduler.scheduleJob(jobDetail , trigger);
Thread.sleep(5000);
// 删除job
scheduler.deleteJob(jobKey);
}
Quartz 主要包含以下几个部分
Job
是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;
JobDetail
Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。
Trigger
是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;
Calendar
org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。
Scheduler
代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。
Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例;
ThreadPool
Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
3、Spring定时
注解版本
<task:annotation-driven />
配置扫描任务位置
<!-- 扫描任务 -->
<context:component-scan base-package="com.vrveis.roundTrip.task" />
package com.vrveis.roundTrip.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class FlightTrainTask {
@Scheduled(cron = "0/5 * * * * ? ") // 间隔5秒执行
public void taskCycle() {
System.out.println("使用Spring框架配置定时任务");
}
}
Xml版本
<context:component-scan base-package="com" />
<!-- spring框架的Scheduled定时器 -->
<task:scheduled-tasks>
<task:scheduled ref="springTiming" method="test" cron="0 0 12 * * ?"/>
</task:scheduled-tasks>
public class SpringTiming {
public void test(){
System.out.println("使用Spring定时任务");
}
}
4、SpringBoot
pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
启动类添加@EnableScheduling 注解
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
创建定时任务类
@Component
public class SchedulerTask {
private Logger logger = LoggerFactory.getLogger(SchedulerTask.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
private int count = 0;
@Scheduled(cron="*/6 * * * * ?")
private void process(){
logger.info("this is scheduler task runing "+(count++));
}
@Scheduled(fixedRate = 6000)
public void reportCurrentTime() {
logger.info("现在时间:" + dateFormat.format(new Date()));
}
}
运行结果
2019-01-19 14:49:12.025 INFO 17024 --- [pool-1-thread-1]
c.example.springbootdemo.SchedulerTask : this is scheduler task runing 0
2019-01-19 14:49:13.109 INFO 17024 --- [pool-1-thread-1]
c.example.springbootdemo.SchedulerTask : 现在时间:14:49:13
通过运行结果看到是一个线程执行定时任务,如果在同一时间启动多个定时器怎么办?
增加一个配置类
@Configuration
//所有的定时任务都放在一个线程池中,定时任务启动时使用不同都线程。
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
//设定一个长度10的定时任务线程池
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
测试结果可以看出现在是多个线程运行定时器。
2019-01-19 14:51:30.025 INFO 18392 --- [pool-1-thread-2]
c.example.springbootdemo.SchedulerTask : this is scheduler task runing 0
2019-01-19 14:51:32.214 INFO 18392 --- [pool-1-thread-1]
c.example.springbootdemo.SchedulerTask : 现在时间:14:51:32
Cron表达式

corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
1)*:表示匹配该域的任意值。假如在Minutes域使用*, 即表示每分钟都会触发事件。
2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。
Cron表达式生成器
http://cron.qqe2.com/
 |