一、概述
定时任务的作用是在设定的时间和日期后自动执行任务,执行任务的周期既能是单次也能是周期性。
本文重点说明Timer、ScheduledThreadPoolExecutor、Spring Task、Quartz等几种定时任务技术方案。
二、Timer
JDK自带的Timer是最古老的定时任务实现方式了。Timer是一种定时器工具,用来在一个后台线程计划执行指定任务。它可以安排任务“执行一次”或者定期“执行多次”。
在实际的开发当中,经常需要一些周期性的操作,比如每5分钟执行某一操作等。对于这样的操作最方便、高效的实现方式就是使用java.util.Timer工具类。
核心方法:
// 在指定延迟时间后执行指定的任务
schedule(TimerTask task,long delay);
// 在指定时间执行指定的任务。(只执行一次)
schedule(TimerTask task, Date time);
// 延迟指定时间(delay)之后,开始以指定的间隔(period)重复执行指定的任务
schedule(TimerTask task,long delay,long period);
// 在指定的时间开始按照指定的间隔(period)重复执行指定的任务
schedule(TimerTask task, Date firstTime , long period);
// 在指定的时间开始进行重复的固定速率执行任务
scheduleAtFixedRate(TimerTask task,Date firstTime,long period);
// 在指定的延迟后开始进行重复的固定速率执行任务
scheduleAtFixedRate(TimerTask task,long delay,long period);
// 终止此计时器,丢弃所有当前已安排的任务。
cancal();
// 从此计时器的任务队列中移除所有已取消的任务。
purge();
总结:
(1)Timer的方法整体可以分为按延时时间和日期时间两种执行任务,其参数分别对应long delay、Date time;
(2)其中schedule()方法是按照固定间隔来定时执行任务,而scheduleAtFixedRate()方法是按照固定速率来定时执行任务的,他们的区别是如果任务执行时间比较长,已经执行到下一个周期了,schedule()方法执行的任务错过了就错过了,而scheduleAtFixedRate()方法则会努力赶上,保障周期内的任务执行速率固定;
代码示例:
/**
* @author yangnk
* @desc
* @date 2023/09/06 23:00
**/
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
Date date = new Date();
System.out.println("before date = " + date.toString());
timer.schedule(new TimerTask() {
@Override
public void run() {
Date date = new Date();
System.out.println("after date = " + date.toString());
System.out.println("task thread name = " + Thread.currentThread().getName());
}
}, 3000);
}
}
原理:Timer做定时任务的原理是使用的Object.wait(timeout),来进行的线程阻塞实现的,他的实现是单线程模式。
三、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。
ScheduledThreadPoolExecutor的层级结果如下:
核心方法:
ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
<V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnitunit);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnitunit);
总体来说,ScheduledThreadPoolExecutor中实现的方法和Timer差不多,都能够实现按照延时和日期来执行任务,也区分按照固定延时时间和固定速率来执行任务。但和Timer的最大区别是ScheduledThreadPoolExecutor的定时任务是是多线程执行的,每开始执行一个定时任务,他就会从线程池中取一个线程来执行,这样的话他就不存在一个定时任务延期影响后一个定时任务的情况了。他的原理是使用 DelayQueue
作为延时任务队列,等时间到了再创建工作线程执行。
JDK原生的定时任务实现方式Timer和ScheduledThreadPoolExecutor最大的问题是它不支持cron表达式和持久化机制,这个在下面的Spring Task和Quartz中得到了解决。
四、Spring Task
从Spring 3开始,Spring自带了一套定时任务工具Spring Task,可以把它看成是一个轻量级的Quartz,使用起来十分简单,除Spring相关的包外不需要额外的包,支持注解和配置文件两种形式。通常情况下在Spring体系内,针对简单的定时任务,可直接使用Spring提供的功能。
在项目实践中通常是在需要做定时任务的方法上添加@Scheduled注解,并在启动类上添加@EnableScheduling注解。
代码实现:
//任务实现
/**
* @author yangnk
* @desc
* @date 2023/09/07 15:11
**/
@Service
@Slf4j
public class SpringTaskTest {
/**
* 使用Cron表达式,每3s执行一次
*/
@Scheduled(cron = "0/3 * * * * ? ")
public void job1() {
log.info("cron job, the time is now {}", new Date());
}
/**
* 延迟3s执行
*/
@Scheduled(fixedDelay = 3000L)
public void job2() {
log.info("fixedDelay job, the time is now {}", new Date());
}
/**
* 延迟3s执行,按照规定速率
*/
@Scheduled(fixedRate = 3000L)
public void job3() {
log.info("fixedRate job, the time is now {}", new Date());
}
}
Spring Task底层是基于ThreadPoolTaskScheduler来实现,可以自定义线程池的大小等参数,只需要实现SchedulingConfigurer接口即可。
//定时任务线程池配置类
/**
* @author yangnk
* @desc
* @date 2023/09/07 15:24
**/
@Configuration
public class TaskConfig implements SchedulingConfigurer {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(10);
executor.setThreadNamePrefix("my-task-thread");
//设置饱和策略
//CallerRunsPolicy:线程池的饱和策略之一,当线程池使用饱和后,直接使用调用者所在的线程来执行任务;如果执行程序已关闭,则会丢弃该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
//配置@Scheduled 定时器所使用的线程池
//配置任务注册器:ScheduledTaskRegistrar 的任务调度器
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
//可配置两种类型:TaskScheduler、ScheduledExecutorService
//scheduledTaskRegistrar.setScheduler(taskScheduler());
//只可配置一种类型:taskScheduler
scheduledTaskRegistrar.setTaskScheduler(taskScheduler());
}
}
总结:Spring task支持延时下发任务和cron定时执行任务,但Spring task 本身不支持持久化,也没有推出官方的分布式集群模式,只能靠开发者在业务应用中自己手动扩展实现,无法满足可视化,易配置的需求。
五、Quartz
参考本人所写的《分布式定时任务框架Quartz总结和实践》系列文章。
六、Linux Cron
Linux Cron也是一种非常普遍的实现定时任务的方式,实际是操作系统的定时任务。Linux Cron只能到达分钟级,到不了秒级别。
这个经常会用在Linux定时备份和巡检相关业务。
七、时间轮
时间轮是一种算法思想,在Kafka、Dubbo、ZooKeeper、Netty 、Caffeine 、Akka 中都有对时间轮的实现。
时间轮简单来说就是一个环形的队列(底层一般基于数组实现),队列中的每一个元素(时间格)都可以存放一个定时任务列表。
时间轮中的每个时间格代表了时间轮的基本时间跨度或者说时间精度,加入时间一秒走一个时间格的话,那么这个时间轮的最高精度就是 1 秒(也就是说 3 s 和 3.9s 会在同一个时间格中)。
时间轮比较适合任务数量比较多的定时任务场景,它的任务写入和执行的时间复杂度都是 0(1)。
八、总结
Timer | ScheduledThreadPoolExecutor | Spring Task | Quartz | Linux Cron | |
---|---|---|---|---|---|
优点 | JDK原生自带,简单轻便 | JDK原生自带,简单轻便 | Spring框架实现,和springboot集成非常简单,支持cron表达式 | Quartz功能非常丰富,支持cron表达式、支持持久化和分布式部署 | Linux原生自带,简单便捷 |
缺点 | 单线程实现,不支持cron表达式和持久化机制 | 线程池实现,不支持cron表达式和持久化机制 | 不支持持久化机制 | 框架比较重,需要依赖外部组件,有较高的耦合性 | 功能单一,不保障可靠性 |
参考资料
- Java中定时任务的6种实现方式,你知道几种?:https://juejin.cn/post/6992719702032121864#heading-16 (主要参考)
- Java 定时任务框架大揭秘!| JavaGuide:https://zhuanlan.zhihu.com/p/414296662 (时间轮算法)
- Java之旅–定时任务(Timer、Quartz、Spring、LinuxCron):https://www.kancloud.cn/digest/java-travel/159427 (linux cron)
本文由博客一文多发平台 OpenWrite 发布!