在 Spring Boot 中,定时任务的实现方案多种多样,本文主要基于单机模式环境下讲述,所谓单机就是一个Java应用服务,至于集群分布式定时任务之前有总结过,感兴趣的可去公众号自行查看。
关于单机定时任务实现方式有如下几种:
ScheduledExecutorService
和 Timer
@Scheduled
注解实现定时任务Timer
是比较古老的定时任务工具,不推荐使用了,在 Java 1.3 引入的用于执行定时任务。Timer
是通过单线程调度任务的,使用 Timer
类和 TimerTask
类配合实现定时任务。示例如下:
public class TimerTest {
public static void main(String[] args) {
TimerTask task1 = new TimerTask() {
@SneakyThrows
@Override
public void run() {
System.out.println("task1 run:"+ new Date());
TimeUnit.SECONDS.sleep(6);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
System.out.println("task2 run:"+ new Date());
}
};
System.out.println("开始执行了。。。" + new Date());
Timer timer = new Timer();
//安排指定的任务在指定的时间开始进行重复的固定延迟执行。这里是0秒延时即立即执行,每10秒执行一次
timer.schedule(task1,0,10000);
timer.schedule(task2,0,10000);
}
}
执行结果:
开始执行了。。。Mon Dec 16 10:32:19 CST 2024
task1 run:Mon Dec 16 10:32:19 CST 2024
task2 run:Mon Dec 16 10:32:25 CST 2024
可以看出task1和task2虽然都是同时启动执行任务,但是执行时间相隔6s,正好是task1执行任务睡眠的时间,这有力的说明了Timer
是单线程执行任务的,也就是task1执行完了,task2才执行。
public class TimerTest {
public static void main(String[] args) {
TimerTask task1 = new TimerTask() {
@SneakyThrows
@Override
public void run() {
System.out.println("task1 run:"+ new Date());
TimeUnit.SECONDS.sleep(6);
throw new RuntimeException("task1 error...");
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
System.out.println("task2 run:"+ new Date());
}
};
System.out.println("开始执行了。。。" + new Date());
Timer timer = new Timer();
//安排指定的任务在指定的时间开始进行重复的固定延迟执行。这里是每10秒执行一次
timer.schedule(task1,0,10000);
timer.schedule(task2,0,10000);
}
}
执行结果:
开始执行了。。。Mon Dec 16 10:39:26 CST 2024
task1 run:Mon Dec 16 10:39:26 CST 2024
Exception in thread "Timer-0" java.lang.RuntimeException: task1 error...
at com.shepherd.basedemo.schedule.TimerTest$1.run(TimerTest.java:25)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
Process finished with exit code 0
这里我给出了控制台的全部输出结果,由此可见在task1报错了,整个任务就停掉了,既没有执行task2,也没有按照定时需求10s后再次执行,这显然是不行的。其实在阿里巴巴Java开发手册中就有明确规定不再允许使用Timer
来实现定时任务。
ScheduledExecutorService
是 Java 1.5 引入的,是 java.util.concurrent
包的一部分。提供线程池支持,允许多个任务并行执行。是现代化、高效、线程安全的任务调度工具,推荐使用。简单来说就是该类是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。话不多说直接看示例:
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
// 任务1:延时任务
5秒后执行,只执行1次
scheduler.schedule(() -> System.out.println("task1 run: " + DateUtil.formatDateTime(new Date()) + " threadName:" + Thread.currentThread().getName()), 5, TimeUnit.SECONDS);
// 任务2:延迟1秒后执行,每隔2秒执行一次
scheduler.scheduleAtFixedRate(() -> {
System.out.println("task2 run: " + DateUtil.formatDateTime(new Date()) + " threadName:"
+ Thread.currentThread().getName());
}, 1, 2, TimeUnit.SECONDS);
// 任务3:上一个任务结束后,延迟2秒执行
scheduler.scheduleWithFixedDelay(() -> {
System.out.println("task3 run: " + DateUtil.formatDateTime(new Date()) + " threadName:"
+ Thread.currentThread().getName());
}, 1, 2, TimeUnit.SECONDS);
}
执行结果:
task2 run: 2024-12-16 11:40:06 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:06 threadName:pool-1-thread-2
task2 run: 2024-12-16 11:40:08 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:08 threadName:pool-1-thread-3
task2 run: 2024-12-16 11:40:10 threadName:pool-1-thread-1
task1 run: 2024-12-16 11:40:10 threadName:pool-1-thread-2
task3 run: 2024-12-16 11:40:10 threadName:pool-1-thread-3
task2 run: 2024-12-16 11:40:12 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:12 threadName:pool-1-thread-2
task2 run: 2024-12-16 11:40:14 threadName:pool-1-thread-3
task3 run: 2024-12-16 11:40:14 threadName:pool-1-thread-1
task2 run: 2024-12-16 11:40:16 threadName:pool-1-thread-1
task3 run: 2024-12-16 11:40:16 threadName:pool-1-thread-3
从结果上来看ScheduledExecutorService
确实是多线程的,同一时间两个任务执行顺序不定且互相独立。使用起来非常简单直接。我们也注意到task1有且仅执行了一次,这不就是妥妥的延时任务吗,需要实现简单延时任务完全可以使用它来搞定,比使用消息队列rabbitMQ, rocketMQ
基于死信队列
实现延时任务处理来的快多了。
既然ScheduledExecutorService
是当前Java提供的主流定时任务并推荐使用,我们这里就来好好分析下吧,先来来看看其定义如下所示:
public interface ScheduledExecutorService extends ExecutorService {
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
schedule(Runnable command, long delay, TimeUnit unit)
:延迟执行任务。scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
:以固定的速率重复执行任务。scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
:以固定的延迟重复执行任务。参数含义:这里也scheduleAtFixedRate
方法为例
Runnable command
: 任务体,也就是定时任务执行的核心逻辑long initialDelay
: 首次执行的延时时间long period
: 任务执行间隔TimeUnit unit
: 首次延时执行和周期间隔时间单位该接口继承了Java并发包线程池工具封装的上次接口ExecutorService
,这就意味着ScheduledExecutorService
拥有多线程处理任务的能力,这和我们上面的介绍是吻合的。其核心实现类是:
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
}
可以看到ScheduledThreadPoolExecutor
继承了ThreadPoolExecutor
,ThreadPoolExecutor
不正是Java并发包实现线程池的核心类吗,不清楚的可以跳转之前总结的文章查看:
关于ScheduledThreadPoolExecutor
源码解读,碍于篇幅问题这里就不做过度解读了,有兴趣可以直接去看看源码。
如果需要取消任务,可以使用 Future
对象:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
ScheduledFuture<?> future = scheduler.schedule(() -> System.out.println("Task executed"), 5, TimeUnit.SECONDS);
// 取消任务
future.cancel(false);
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构
底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
上面讲的两个单机定时任务工具都是JDK提供的,接下来我们来看看Spring Scheduler
,它是 Spring 框架中提供的一种定时任务实现,基于 Spring 的功能封装,便于集成到 Spring 应用中,可以这么说我们在平时使用Spring Boot
开发系统中一定使用过@Scheduled
实现定时任务,直接上示例:
@Component
@Slf4j
public class ScheduledTask {
// 每隔5秒执行一次
@Scheduled(fixedRate = 5000)
public void taskWithFixedRate() {
log.info("Fixed Rate Task: " + DateUtil.formatDateTime(new Date()));
}
// 首次延时3s执行,上次任务结束后5秒再执行
@Scheduled(fixedDelay = 5000, initialDelay = 3000)
public void taskWithFixedDelay() {
log.info("Fixed Delay Task: " + DateUtil.formatDateTime(new Date()));
}
// 使用 cron 表达式
, 每10秒执行一次
@Scheduled(cron = "0/10 * * * * ?")
public void taskWithCron() {
log.info("Cron Task: " + DateUtil.formatDateTime(new Date()));
}
}
在项目启动类上加上注解@EnableScheduling
@SpringBootApplication
@EnableScheduling
public class BaseDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BaseDemoApplication.class, args);
}
}
参数说明
fixedRate
:固定速率执行任务,不考虑任务的执行时间。fixedDelay
:固定延迟执行任务,即上次任务完成后等待指定时间再执行。cron
:使用 Cron 表达式定义任务调度启动项目执行结果如下:
[common-demo] [] [2024-12-16 16:44:59.973] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:44:59
[common-demo] [] [2024-12-16 16:45:00.005] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: Cron Task: 2024-12-16 16:45:00
[common-demo] [] [2024-12-16 16:45:02.892] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:02
[common-demo] [] [2024-12-16 16:45:04.891] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:04
[common-demo] [] [2024-12-16 16:45:07.901] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:07
[common-demo] [] [2024-12-16 16:45:09.890] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:09
[common-demo] [] [2024-12-16 16:45:10.001] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: Cron Task: 2024-12-16 16:45:10
[common-demo] [] [2024-12-16 16:45:12.904] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:12
[common-demo] [] [2024-12-16 16:45:14.890] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:14
[common-demo] [] [2024-12-16 16:45:17.907] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:17
[common-demo] [] [2024-12-16 16:45:19.891] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedRate: Fixed Rate Task: 2024-12-16 16:45:19
[common-demo] [] [2024-12-16 16:45:20.002] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: Cron Task: 2024-12-16 16:45:20
[common-demo] [] [2024-12-16 16:45:22.911] [INFO] [scheduling-1@37033] com.shepherd.basedemo.schedule.ScheduledTask taskWithFixedDelay: Fixed Delay Task: 2024-12-16 16:45:22
从执行结果输出可以看出三个任务是由同一个线程串行执行的,当定时任务增多,如果一个任务卡死,会导致其他任务也无法执行。同时也注意到了Fixed Delay Task 比Fixed Rate Task晚了3s执行,因为我们配置了initialDelay = 3000
。关于多个任务在同一个线程中串行执行我们再来看看一个示例:
@Component
@Slf4j
public class ScheduledTask {
// 使用 cron 表达式, 每10秒执行一次
@SneakyThrows
@Scheduled(cron = "0/10 * * * * ?")
public void taskWithCron() {
log.info("task1: " + DateUtil.formatDateTime(new Date()));
// 模拟task1执行需要耗费8s
TimeUnit.SECONDS.sleep(8);
}
}
@Component
@Slf4j
public class ScheduledTask2 {
// 使用 cron 表达式, 每10秒执行一次
@Scheduled(cron = "0/10 * * * * ?")
public void taskWithCron() {
log.info("task2: " + DateUtil.formatDateTime(new Date()));
}
}
这里我们定义了两个任务类分别执行定时任务,并且让task1 睡眠了8s模拟任务逻辑耗时,看看task2执行时间:
[common-demo] [] [2024-12-16 17:00:30.126] [INFO] [scheduling-1@42202] com.shepherd.basedemo.schedule.ScheduledTask taskWithCron: task1: 2024-12-16 17:00:30
[common-demo] [] [2024-12-16 17:00:38.138] [INFO] [scheduling-1@42202] com.shepherd.basedemo.schedule.ScheduledTask2 taskWithCron: task2: 2024-12-16 17:00:38
很明显task1和task2还是同一个线程串行执行,所以task2是等着task1执行完之后才执行的。
@Async
实现异步定时任务Spring 的 @Async
注解可以实现异步任务调度,结合 @Scheduled
可以异步执行定时任务。
使用@Async
的时候,一般都会自定义线程池,因为@Async
的默认线程池为SimpleAsyncTaskExecutor
,不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
@Configuration
public class AsyncConfig {
/**
* 初始化一个线程池,放入spring beanFactory
* @return
*/
@Bean(name = "asyncExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("asyncExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
需要在启动类或配置类中开启异步任务:
@SpringBootApplication
@EnableAsync // 开启异步任务
@EnableScheduling // 开启定时任务
public class BaseDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncScheduledTaskApplication.class, args);
}
}
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。