Java定时任务实现方式
定时任务底层的算法基础
- 小顶堆:每一次任务的调度,都需要重新进行剩余任务的重新排序,效率性能较低
- 时间轮算法:链表或者数组实现针对具体到某一天、某一小时、某一分、某一秒的具体定时任务不适合;round时间轮只是缓解了上述的链表或数组实现情况;**分层时间轮(cron表达式底层实现逻辑)**通过设置不同的时间轮,分层抽取调度,大大加快了任务调度的效率。
常见的定时任务实现方式
- JDK自带的Timer以及ScheduledExecutorService
- Quartz:Java作业调度框架
- Spring3.0自带的SpringTask(轻量级的Quartz)
Timer类
Timer类实现定时任务主要是通过其内部的schedule
方法实现,而schedule
方法参数一般存在三个:
- TimeTask类型的对象
- delay(long类型,表示延时多久开始执行)
- period(long类型,表示定时任务执行的周期)
对于TimeTask类而言,其本身是一个抽象类
,实现了Runnable
接口,内部存在一个run
方法,完成的即是定时任务的具体工作
//自定义一个Task任务类
public class Task extends TimerTask {
@Override
public void run() {
System.out.println("task01 running...");
}
}
//进行定时任务的调用
public class TaskTest {
public static void main(String[] args) {
Timer timer = new Timer();
//delay:延迟时间(ms),period:执行周期
timer.schedule(new Task(),1000,5000);
new Timer().schedule(new Task(){
@Override
public void run() {
System.out.println("running...stop....");
}
},1000,2000);
}
}
ScheduledExecutorService接口
ScheduledThreadPoolExecutor
作为ScheduledExecutorService接口的实现类,提供了对应的schedule
方法:
public class ScheduledThreadPoolExecutorTest {
public static void main(String[] args) {
ScheduledThreadPoolExecutor task = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(5);
//5个线程
for (int i = 0; i < 5; i++) {
//同一时间有五条任务进行调度
task.schedule(()->{
System.out.println(Thread.currentThread().getName() + "running...");
},1000, TimeUnit.MILLISECONDS);
}
}
}
QuartZ
Quartz是一个完全由Java编写的开源作业调度框架
,为在Java应用程序中进行作业调度提供了简单却强大的机制。Quartz允许开发人员根据时间间隔
来调度作业。它实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。
在QuartZ中存在主要的三个核心类:
-
Scheduler:调度器。所有的调度都是由它控制。其中,Trigger和JobDetails可以注册打到Scheduler中,两者在Scheduler中拥有各自的组及名称,且保证Trigger和JobDetails的组和名称保证唯一性。
-
Trigger: 定义触发的条件。主要有
SimpleTrigger
和CronTrigger
这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等 -
JobDetail & Job: JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。
- 为什么设计成
JobDetail + Job
,不直接使用Job?这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。
如果你是SpringBoot2.0以上的版本,那么只需引入以下依赖即可:
- 为什么设计成
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
我们先定义一个TastJob
public class TaskJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("定时任务开始执行中...");
//获取jobDetails和jobDataMap
JobDetail jobDetail = jobExecutionContext.getJobDetail();
JobDataMap jobDataMap = jobDetail.getJobDataMap();
//执行定时任务...这里打印对应的Job细节信息
System.out.println("name: " + jobDataMap.get("name") +
"job: " + jobDataMap.get("job") +
"level:" + jobDataMap.get("level"));
}
}
然后定义TestSchedule
进行任务调度:
public class TestSchedule {
public static void main(String[] args) throws SchedulerException {
//第一步:获取调度工厂
StdSchedulerFactory factory = new StdSchedulerFactory();
//第二步:通过工厂获取调度器
Scheduler scheduler = factory.getScheduler();
//第三步:创建对应的JobDetails
JobDetail jobDetail= JobBuilder.newJob(TaskJob.class)
.withIdentity("details","group1")
.withDescription("this is a jobDetail")
.build();
//设置具体的工作参数
jobDetail.getJobDataMap().put("name","zhuzhu");
jobDetail.getJobDataMap().put("job","job1");
jobDetail.getJobDataMap().put("level","level1");
//设置开始时间
Long currentTime = System.currentTimeMillis() + 6 * 1000L;
Date time = new Date(currentTime);
//第四步:创建Trigger(调度规则)
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger","group01")
//任务开始时间
.startAt(time)
.withDescription("this is a trigger")
//调度设置1:使用SimpleScheduleBuilder调度器,每隔2秒重复执行3次
.withSchedule(SimpleScheduleBuilder.simpleSchedule().
withIntervalInSeconds(2).withRepeatCount(3))
//调度设置2:使用CronScheduleBuilder调度器,每5秒调度一次
//.withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
.build();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
}
}
注意两个注解:
- @DisallowConcurrentExecution:禁止并发地执行同一个job定义,即进行Job实例创建的是同一个实例,而非每一次创建一个不同的实例
- @PersistJobDataAfterExecution:持久化JobDetail中的JobDataMap,公用同一个JobDetailMap实例
SpringTask
SpringTask默认在无任何第三方依赖的情况下使用spring-context
模块下提供的定时任务工具Spring Task
。我们只需要使用@EnableScheduling
注解就可以开启相关的定时任务功能。如:
@SpringBootApplication
@EnableScheduling
public class SpringbootScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootScheduleApplication.class, args);
}
}
此时,只需要定义一个Spring Bean,然后定义具体的定时任务逻辑方法就可以使用@Scheduled
注解标记该方法即可
package cn.felord.schedule.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class TaskService {
@Scheduled(fixedDelay = 1000)
public void task() {
System.out.println("Thread Name : "
+ Thread.currentThread().getName() + " i am a task : date -> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
其中,@Scheduled
中提供了四种属性:
- cron表达式
- fixedDelay : 定时任务开启是根据上次的任务结束的时间开始计算的,只需要关注上一次任务的结束时间即可
- fixedRate : 由于默认SpringBoot的定时任务是单线程执行的,这里下一次开始和上一次开始的间隔是一定的,如果本次任务超时完成,则下一次任务的等待时间就会被压缩设置阻塞。
- initDelay:初始化延迟时间,也就是第一次延迟执行的时间,该参数对
cron
属性无效,只能配合后两个实现。
弊端:
- 多线程下影响定时策略
- 默认不支持分布式,在分布式环境下,SpringTask的定时任务不支持集群配置,如果配置到多个节点上,各个节点上并不存在任何通讯机制,集群的节点之间是不会共享任务信息的,每一个节点上的任务都会按时执行,导致任务的重复执行。此时可以使用QuartZ,XXL-Job,Elastic-Job。当然可以借助Zookeeper,redis来实现分布式锁来处理各种节点的协调问题。或者将所有的定时任务抽成单独的服务单独部署。
定时任务不支持集群配置,如果配置到多个节点上,各个节点上并不存在任何通讯机制,集群的节点之间是不会共享任务信息的,每一个节点上的任务都会按时执行,导致任务的重复执行。此时可以使用QuartZ,XXL-Job,Elastic-Job。当然可以借助Zookeeper,redis来实现分布式锁来处理各种节点的协调问题。或者将所有的定时任务抽成单独的服务单独部署。
解决方式:
为了让每一个定时任务在不同线程运行,不受干扰,可以添加以下配置类:
@Configuration
@EnableAsync
public class AsyncConfig {
/*
此处成员变量应该写到我们的properties配置文件当中,然后使用@Value从配置中读取
*/
private int corePoolSize = 10;
private int maxPoolSize = 200;
private int queueCapacity = 10;
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.initialize();
return executor;
}
}
然后在定时任务的类或者方法上添加@Async
。最后重启项目。此时,每一个任务都是在不同的线程中。
个人建议使用QuartZ(SpringBoot2.0以上版本)或者SpringTask进行定时任务的编写