文章目录
1.Timer 类
介绍
- 定时任务由 Timer 和 TimerTask 两个类配合完成。
- 创建一个 Timer 就是启动一个新线程,其中创建Timer时传true,可以设置此线程为守护线程;TimerTask类为抽象类,实现了Runnable接口,主要用来创建一个新的线程执行任务。
- Date time 为执行任务的日期,如果该日期早于当前时间,那么程序启动后,任务会立即执行
- 一个Timer可以执行多个TimerTask任务,TimerTask任务以队列的方式一个个被顺序执行,当前面任务执行时间较长时,会使得后边任务的运行时间也会被延迟,从而出现执行时间于预期时间不一致的情况
schedule方法:
(1)schedule(TimerTask task, Date time):安排在指定的时间执行指定的任务
(2)schedule(TimerTask task, Date firstTime, long period):安排指定的任务在指定的时间开始进行重复的固定延迟执行
(3)schedule(TimerTask task, long delay):安排在指定延迟后执行指定的任务
(4)schedule(TimerTask task, long delay, long period):安排指定的任务在指定的延迟后开始进行重复的固定速率执行
实例
@Component
public class TimerTest {
private Timer timer1 = new Timer();
private Timer timer2 = new Timer();
public void timerTest1() {
LocalDateTime localDateTime = LocalDateTime.of(2023,1,12,15,16);
Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date startTime = Date.from(instant);
System.out.println(startTime);
timer1.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("==================Timer定时任务1执行:" + LocalDateTime.now());
}
}, 10000, 20000);
}
public void timerTest2() {
LocalDateTime localDateTime = LocalDateTime.of(2023,1,12,15,17);
Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date startTime = Date.from(instant);
System.out.println(startTime);
timer1.schedule(new TimerTask() {
@SneakyThrows
@Override
public void run() {
System.out.println("==================Timer定时任务2执行:" + LocalDateTime.now());
}
}, 5000, 10000);
}
public void timerTest3() {
LocalDateTime localDateTime = LocalDateTime.of(2023,1,12,15,17);
Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date startTime = Date.from(instant);
System.out.println(startTime);
timer2.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("==================Timer定时任务3执行:" + LocalDateTime.now());
}
}, 2000, 5000);
}
public void timerTest3() {
LocalDateTime localDateTime = LocalDateTime.of(2023,1,12,15,17);
Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date startTime = Date.from(instant);
System.out.println(startTime);
timer2.schedule(new TimerTask() {
@SneakyThrows
@Override
public void run() {
System.out.println("==================Timer定时任务4执行:" + LocalDateTime.now());
throw new Exception();
}
}, 1000, 5000);
}
}
缺点
- Timer中的一个TimerTask抛出异常,则该Timer不会再运行
- 单点部署
2.ScheduledExecutorService
介绍
ScheduledExecutor的设计思想是每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发的,相互之间不会受到干扰;只有当任务的时间到来时,ScheduledExecutor才会真正启动一个线程。
实例
@Component
public class ScheduledExecutorServiceTest {
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
// 一次性任务,它会在指定延迟后只执行一次
public void scheduledTest1() {
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("==================scheduled定时任务1执行:" + LocalDateTime.now());
}
}, 5, TimeUnit.SECONDS);
}
// 2秒后开始执行定时任务,每3秒执行,任务总是以固定时间间隔触发,不管任务执行多长时间
public void scheduledTest2() {
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("==================scheduled定时任务2执行:" + LocalDateTime.now());
}
}, 5,5, TimeUnit.SECONDS);
}
// 2秒后开始执行定时任务,以3秒为间隔执行,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务
public void scheduledTest3() {
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println("==================scheduled定时任务3执行:" + LocalDateTime.now());
}
}, 5,5, TimeUnit.SECONDS);
}
// 关闭线程池
public void shutdown() {
scheduledExecutorService.shutdown();
}
}
缺点
- 单点部署
3.Spring Task
开启定时任务
Spring Boot 默认在无任何第三方依赖的情况下使用 spring-context 模块下提供的定时任务工具 Spring Task。启动类加上 @EnableScheduling 注解。
自定义调度器线程池
@Scheduled 任务调度注解,主要用于配置定时任务;springboot默认的调度器线程池大小为 1。(也就是说在多个方法上加上@schedule的,多个定时任务默认是加入延时队列依次同步执行的)。所以,如果在使用springboot定时器时,有多个定时任务,在使用默认的调度器配置,就会出现排队现象,因为同时只能有一个任务在执行,这个时候当一个任务挂死,那后面的定时任务就不能有效执行了。解决办法就是自定义调度器线程池。
源码分析
// 默认使用的调度器
if(this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
// 可以看到SingleThreadScheduledExecutor指定的核心线程为1,说白了就是单线程执行
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
// 利用了DelayedWorkQueue延时队列作为任务的存放队列,这样便可以实现任务延迟执行或者定时执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
(1)增加配置类,注入TaskScheduler Bean
@Configuration
public class ScheduleConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
//核心线程池数量,方法: 返回可用处理器的Java虚拟机的数量。
taskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors() * 2);
taskScheduler.setThreadNamePrefix("scheduled-thread-");
//设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
//设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住
taskScheduler.setAwaitTerminationSeconds(60);
return taskScheduler;
}
}
(2)注入SchedulingConfigurer Bean
@Configuration
public class ScheduleConfig1 implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
// 直接指定
scheduledTaskRegistrar.setScheduler(
new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors() * 2)
);
//也可以自定义的线程池,方便线程的使用与维护,这里不多说了
scheduledTaskRegistrar.setTaskScheduler(myThreadPoolTaskScheduler);
}
@Bean(name = "myThreadPoolTaskScheduler")
public TaskScheduler getMyThreadPoolTaskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);
taskScheduler.setThreadNamePrefix("Haina-Scheduled-");
taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//调度器shutdown被调用时等待当前被调度的任务完成
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
//等待时长
taskScheduler.setAwaitTerminationSeconds(60);
return taskScheduler;
}
}
(3)配置文件添加task配置
# 任务调度线程池
# 任务调度线程池大小 默认 1 建议根据任务加大
spring.task.scheduling.pool.size=1
# 调度线程名称前缀 默认 scheduling-
spring.task.scheduling.thread-name-prefix=scheduling-
# 线程池关闭时等待所有任务完成
spring.task.scheduling.shutdown.await-termination=true
# 调度线程关闭前最大等待时间,确保最后一定关闭
spring.task.scheduling.shutdown.await-termination-period=60
# 任务执行线程池配置
# 是否允许核心线程超时。这样可以动态增加和缩小线程池
spring.task.execution.pool.allow-core-thread-timeout=true
# 核心线程池大小 默认 8
spring.task.execution.pool.core-size=8
# 线程空闲等待时间 默认 60s
spring.task.execution.pool.keep-alive=60s
# 线程池最大数 根据任务定制
# spring.task.execution.pool.max-size=
# 线程池 队列容量大小
# spring.task.execution.pool.queue-capacity=
# 线程池关闭时等待所有任务完成
spring.task.execution.shutdown.await-termination=true
# 执行线程关闭前最大等待时间,确保最后一定关闭
# spring.task.execution.shutdown.await-termination-period=
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-
自定义调度器线程池的问题
自定义调度器的方式,会有一个问题:当有足够的空余线程时,多任务时并行执行,但是同一定时任务仍会同步执行(当定时任务的执行时间大于每次执行的时间间隔时即可发现);
配合@Async 注解使用,这样在每次执行定时任务时就新开一个线程,异步非阻塞运行;同时使用这两个注解的效果,相当于@Scheduled仅仅负责调度,而@Async指定的executor负责任务执行,不再使用调度器中的执行器来执行任务。
自定义执行器线程池
- 在启动类加上 @EnableAsync 注解
- 在需要异步执行的方法上加 @Async 注解
@Bean("taskExecutor")
public TaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveTime);
executor.setThreadNamePrefix(threadNamePrefix);
// 线程池对拒绝任务的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
实例
@Component
public class SpringTaskTest {
@Scheduled(cron = "0 37 10 * * ?")
@Async("taskExecutor") // 指定执行器线程池
public void SpringTaskTest1() {
System.out.println("==================Timer定时任务1执行:" + LocalDateTime.now());
try {
Thread.sleep(10000);
} catch (Exception e) {
}
}
@Scheduled(fixedDelay = 5000, initialDelay = 1000)
@Async("taskExecutor")
public void SpringTaskTest2() {
System.out.println("==================Timer定时任务2执行:" + LocalDateTime.now());
try {
Thread.sleep(10000);
} catch (Exception e) {
}
}
@Scheduled(fixedRate = 5000, initialDelay = 1000)
@Async("taskExecutor")
public void SpringTaskTest3() {
System.out.println("==================Timer定时任务3执行:" + LocalDateTime.now());
try {
Thread.sleep(10000);
} catch (Exception e) {
}
}
}
Spring Task缺点
- 默认不支持分布式。Spring Task 并不是为分布式环境设计的,在分布式环境下,这种定时任务是不支持集群配置的,如果部署到多个节点上,各个节点之间并没有任何协调通讯机制,集群的节点之间是不会共享任务信息的,每个节点上的任务都会按时执行,导致任务的重复执行。我们可以使用支持分布式的定时任务调度框架,比如 Quartz、XXL-Job、Elastic Job。当然你可以借助 zookeeper 、 redis 等实现分布式锁来处理各个节点的协调问题。或者把所有的定时任务抽成单独的服务单独部署。
基于 Redis 的分布式实现
https://blog.csdn.net/weixin_36380516/article/details/123320936
动态定时任务的实现
https://zhuanlan.zhihu.com/p/61526583