定时任务
引言
现代的应用程序已经从最简单的crud操作改变为程序自动定时定点的调度与执行。
很多业务需求已经离不开定时任务的处理,如每天汇总日志,每月的移动数据,不可能人为的去处理这些,这个时候定时任务能够很好的解决这个问题。
java开发的定时任务的方式主要有以下几种:
- timer
- ScheduledExecutorService
- Spring Task
- Quartz
所以接下来依次进行介绍!
timer
KnowPoint
timer是基于jdk中java.util.Timer的类。
具体的调度方法:
- 在特定的时间之后执行任务,并且只执行一次。
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
- 在指定的时间执行任务,只执行一次
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
- 在特定延迟之后第一次执行,然后按照间隔时间,重复执行。
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, -period);
}
- 执行第一次执行后,然后按照间隔时间,重复执行。
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
- 执行第一次执行,然后按照间隔时间,重复执行。
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, period);
}
- 第一次执行执行之后,特定频率执行
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), period);
}
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}
queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}
相信各位看到这里对于schedule以及scheduleAtFixedRate有一点疑惑,感觉方法功能有点相似。但是还是有一点的区别:
✔假如指定开始执行的时间在当前系统时间之前呢,scheduleAtFixedRate会把已经过去的时间作为周期执行,而schedule并不会。
实例:
一秒执行一次
@GetMapping("Task/Timer")
public String doTimer(){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
log.info("timer定时任务启动"+new Date()+"线程名称:"+Thread.currentThread().getName());
}
},1000,1000);
return "timer";
}
结果:
ScheduledExecutorService
knowPoint
由于Timer不支持多线程,所以ScheduledExecutorService的出现可以说把Timer替代了。
直接上例子
实例
@GetMapping("/Task/sch")
public String Scheula(){
// 设置最大的线程数
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
// 设置选择定时的服务
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
log.info("任务1:"+"ScheduledExecutorService定时任务执行:" + new Date()+"当前线程名称"+Thread.currentThread().getName());
}
}, 500, 500, TimeUnit.MILLISECONDS);//首次延迟0.5秒,之后每0.5秒执行一次
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
log.info("任务2:"+"ScheduledExecutorService定时任务执行:" + new Date()+"当前线程名称"+Thread.currentThread().getName());
}
}, 500, 500, TimeUnit.MILLISECONDS);//首次延迟0.5秒,之后每0.5秒执行一次
log.info("初始:"+"ScheduledExecutorService定时任务启动:" + new Date()+"当前线程名称"+Thread.currentThread().getName());
return "ScheduledExecutorService!";
}
结果:
SpingTask
knowPoint
使用@Scheduled
注解即可轻松搞定
实例
启动springboot自带的定时任务,就需要我们开启这个功能,
启动类加上了@EnableScheduling来开启定时任务功能
之后编写一个启动类,系统启动后会自动扫描,自动执行。
@Component
public class demo {
@Scheduled(fixedRate=500)
public void getCurrentDate() {
log.info("Scheduled定时任务执行:" + new Date()+"线程名:"+Thread.currentThread().getName());
}
}
结果:
这个功能的实现,重点在于@scheduled注解的使用,
点进源码我们可以看到
一共八种类型的参数选择
- cron:顾名思义:接受的为cron表达式
- zone:时区,接收一个
java.util.TimeZone#ID
。cron表达式
会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai
。该字段我们一般留空。 - fixedDelay:上一次执行完毕时间点后多长时间再执行
- fixedDelayString:字符串形式,支持占位
- fixedRate:上一次开始执行时间点之后多长时间再执行
- fixedRateString:字符串形式,支持占位
- initialDelay:第一次延迟多长时间后再执行
- initialDelayString:字符串形式,支持占位
从上面的结果我们可以看出,多任务下,却只有一个线程。
为了可以使用多线程进行任务执行,进行进一步的完善
编写配置类:
@Configuration
@EnableAsync
public class Config {
/**
* 配置线程池
* @return
*/
@Bean(name = "TaskExecutor")
public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(200);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("oKong-Scheduled-");
// 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
//等待时长
taskExecutor.setAwaitTerminationSeconds(60);
taskExecutor.initialize();
return taskExecutor;
}
}
并且方法上加上:
动态添加定时任务
使用注解的方式,无法实现动态的修改或者添加新的定时任务的,这个使用就需要使用编程的方式进行任务的更新操作了。可直接使用ThreadPoolTaskScheduler
或者SchedulingConfigurer
接口进行自定义定时任务创建。
创建一个ThreadPoolTaskScheduler类
@Bean("taskExecutor")
public TaskScheduler taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("oKong-taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待时长
executor.setAwaitTerminationSeconds(60);
return executor;
}
编写控制类:
@Autowired
TaskScheduler taskScheduler;
@GetMapping("/poolTask")
public String threadPoolTaskScheduler() {
taskScheduler.schedule(new Runnable() {
@Override
public void run() {
log.info("ThreadPoolTaskScheduler定时任务:" + new Date());
}
}, new CronTrigger("0/3 * * * * ?"));//每3秒执行一次
return "ThreadPoolTaskScheduler!";
}
访问接口-成功
也可以使用:SchedulingConfigurer
编写配置类:
@Configuration
@Slf4j
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskExecutor());
taskRegistrar.getScheduler().schedule(new Runnable() {
@Override
public void run() {
log.info("SchedulingConfigurer定时任务:" + new Date());
}
}, new CronTrigger("0/3 * * * * ?"));//每3秒执行一次
}
@Bean("taskExecutor")
public TaskScheduler taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("oKong-Executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待时长
executor.setAwaitTerminationSeconds(60);
return executor;
}
}
Quartz
knowPoint
Quartz有四个核心概念:
- Job:是一个接口,只定义一个方法 execute(JobExecutionContext context),在实现接口的 execute 方法中编写所需要定时执行的 Job(任务),JobExecutionContext 类提供了调度应用的一些信息;Job 运行时的信息保存在 JobDataMap 实例中。
- JobDetail:Quartz 每次调度 Job 时,都重新创建一个 Job 实例,因此它不接受一个 Job 的实例,相反它接收一个 Job 实现类(JobDetail,描述 Job 的实现类及其他相关的静态信息,如 Job 名字、描述、关联监听器等信息),以便运行时通过 newInstance() 的反射机制实例化 Job。
- Trigger:是一个类,描述触发 Job 执行的时间触发规则,主要有 SimpleTrigger 和 CronTrigger 这两个子类。当且仅当需调度一次或者以固定时间间隔周期执行调度,SimpleTrigger 是最适合的选择;而 CronTrigger 则可以通过 Cron 表达式定义出各种复杂时间规则的调度方案:如工作日周一到周五的 15:00 ~ 16:00 执行调度等
- Scheduler:调度器就相当于一个容器,装载着任务和触发器,该类是一个接口,代表一个 Quartz 的独立运行容器,Trigger 和 JobDetail 可以注册到 Scheduler 中,两者在 Scheduler 中拥有各自的组及名称,组及名称是 Scheduler 查找定位容器中某一对象的依据,Trigger 的组及名称必须唯一,JobDetail 的组和名称也必须唯一(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。Scheduler 定义了多个接口方法,允许外部通过组及名称访问和控制容器中 Trigger 和 JobDetail
总之:
Job 为作业的接口,为任务调度的对象;JobDetail 用来描述 Job 的实现类及其他相关的静态信息;Trigger 做为作业的定时管理工具,一个 Trigger 只能对应一个作业实例,而一个作业实例可对应多个触发器;Scheduler 做为定时任务容器,是 Quartz 最上层的东西,它提携了所有触发器和作业,使它们协调工作,每个 Scheduler 都存有 JobDetail 和 Trigger 的注册,一个 Scheduler 中可以注册多个 JobDetail 和多个 Trigger。
实例
第一步:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
第二步:构建JobDetail:
@Configuration
public class SampleScheduler {
@Bean
public JobDetail sampleJobDetail() {
// 链式编程,可以携带多个参数,在Job类中声明属性 + setter方法
return JobBuilder.newJob(JobDemo.class).withIdentity("sampleJob")
.usingJobData("name","World").storeDurably().build();
}
@Bean
public Trigger sampleJobTrigger(){
// 每隔两秒执行一次
SimpleScheduleBuilder scheduleBuilder =
SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever();
return TriggerBuilder.newTrigger().forJob(sampleJobDetail()).withIdentity("sampleTrigger")
.withSchedule(scheduleBuilder).build();
}
}
第三步:书写类继承job接口
public class JonFrist implements Job {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
System.out.println("CRON ----> schedule job1 is running ... + " + name + " ----> " + dateFormat.format(new Date()));
}
}
另外一个类似。
构建schedule来执行任务
package com.task.timertask.Quartz;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
@Component
public class CronSchedulerJob {
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
private void scheduleJob1(Scheduler scheduler) throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(JonFrist.class) .withIdentity("job1", "group1").build();
// 6的倍数秒执行 也就是 6 12 18 24 30 36 42 ....
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/6 * * * * ?");
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group1")
.usingJobData("name","测试1").withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail,cronTrigger);
}
private void scheduleJob2(Scheduler scheduler) throws SchedulerException{
JobDetail jobDetail = JobBuilder.newJob(JobSecond.class) .withIdentity("job2", "group2").build();
// 12秒的倍数执行 12 24 36 48 60
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/12 * * * * ?");
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("trigger2", "group2")
.usingJobData("name","测试2").withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail,cronTrigger);
}
/**
* @Author Smith
* @Description 同时启动两个定时任务
* @Date 16:31 2019/1/24
* @Param
* @return void
**/
public void scheduleJobs() throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
scheduleJob1(scheduler);
scheduleJob2(scheduler);
}
}
启动方式:
- 项目启动时运行
@Component
public class MyStartupRunner implements CommandLineRunner {
@Autowired
public CronSchedulerJob scheduleJobs;
@Override
public void run(String... args) throws Exception {
scheduleJobs.scheduleJobs();
System.out.println(">>>>>>>>>>>>>>>定时任务开始执行<<<<<<<<<<<<<");
}
}
CommandLineRunner接口可实现。
- 定时执行
@Configuration
@EnableScheduling
@Component
public class SchedulerListener {
@Autowired
public CronSchedulerJob scheduleJobs;
@Scheduled(cron="0 47 16 24 1 ?")
public void schedule() throws SchedulerException {
scheduleJobs.scheduleJobs();
System.out.println(">>>>>>>>>>>>>>>定时任务开始执行<<<<<<<<<<<<<");
}
}
引出问题:
如果在分布式环境下应该如何解决呢?,请看下篇详解