验证Springboot定时器默认的是单线程的
1.实现源代码
@Component
@EnableScheduling
@Slf4j
public class SchedulerTaskController {
private static final SimpleDateFormat dateFormat=new SimpleDateFormat("HH:mm:ss");
private int count=0;
@Scheduled(cron="*/6 * * * * ?")
@Async("threadPoolTaskExecutor")
public void process(){
log.info("英文:this is scheduler task runing "+(count++));
}
@Scheduled(fixedRate = 6000)
@Async("threadPoolTaskExecutor")
public void currentTime(){
log.info("中文:现在时间"+dateFormat.format(new Date()));
}
}
2.执行结果
3.结论
可以看到所有定时任务都是通过线程scheduling-1来执行的,也就是单线程执行的。也就是说如果不同的定时任务在同一时间执行,就会出现争抢被线程执行权的情况,这样如果某一个线程抢到了被执行权,但是该线程执行时间又特别耗时,这就出现了其它线程一直处于等待状态情况,时间越久,累计等待的定时器越多,严重时引发雪崩。
验证Springboot定时器多线程执行
1.添加配置类
@Configuration
@EnableAsync
public class TaskScheduleConfig {
/**使用多线程的时候,往往需要创建Thread类,或者实现Runnable接口,如果要使用到线程池,我们还需要来创建Executors,
* 在使用spring中,已经给我们做了很好的支持。只要要@EnableAsync就可以使用多线程
* 通过spring给我们提供的ThreadPoolTaskExecutor就可以使用线程池。
*/
private static final int corePoolSize = 10; // 默认线程数
private static final int maxPoolSize = 100; // 最大线程数
private static final int keepAliveTime = 10; // 允许线程空闲时间(单位:默认为秒),十秒后就把线程关闭
private static final int queueCapacity = 200; // 缓冲队列数
private static final String threadNamePrefix = "it-is-threading-"; // 线程池名前缀
@Bean("threadPoolTaskExecutor") // bean的名称,默认为首字母小写的方法名
public ThreadPoolTaskExecutor getThread(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(keepAliveTime);
executor.setKeepAliveSeconds(queueCapacity);
executor.setThreadNamePrefix(threadNamePrefix);
/**
* 线程池拒绝任务的处理策略:
* 当要创建的线程数量大于线程池的最大线程数的时候,新的任务就会被拒绝,就会调用这个接口里的这个方法;
* 也可以自己实现这个接口,实现对这些超出数量的任务的处理。
* ThreadPoolExecutor提供的四种拒绝策略,分别是CallerRunsPolicy,
* AbortPolicy,
* DiscardPolicy,
* DiscardOldestPolicy
*
* AbortPolicy:默认拒绝策略,直接抛出异常,也不执行超出的这个任务
* CallerRunsPolicy:在添加任务被拒绝添加后,会调用当前线程池所在的线程去执行被拒绝的任务,哪个线程
* 创建的线程池就由哪个线程run,如main线程创建线程池,由main去直接run(策略的缺点就是可能会阻塞主线程)
* DiscardPolicy:线程池拒绝的任务直接抛弃,不会抛异常也不会执行
* DiscardOldestPolicy:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。
*/
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//初始化
executor.initialize();
return executor;
}
}
2.定时任务类
@Component
@EnableScheduling
@Slf4j
public class AsyncSchedulerTaskController {
private static final SimpleDateFormat dateFormat=new SimpleDateFormat("HH:mm:ss");
private int count=0;
@Scheduled(cron="*/6 * * * * ?")
@Async("threadPoolTaskExecutor")
public void process(){
log.info("英文:this is scheduler task runing "+(count++));
}
@Scheduled(fixedRate = 6000)
@Async("threadPoolTaskExecutor")
public void currentTime(){
log.info("中文:现在时间"+dateFormat.format(new Date()));
}
}
3.执行结果
4.结论:
可以看到所有的定时任务启动后是由不同的线程执行的,也就解决了用多线程解决Springboot多定时器冲突的问题
Spring @Scheduled定时任务的三种定时类型
1. @Scheduled(cron=“0/5 * * * *?”)
当时间达到设置的时间会触发事件。上面那个例子会每5秒执行一次,也有单线程和多线程的区分。
2. @Scheduled(fixedRate=2000)
项目启动后立即执行一次,之后按照固定的时间间隔(如上设置的2秒)进行执行,但是如果在项目中使用的单线程执行定时任务,那么并不会按照时间间隔启动定时任务,而是等上一个执行结束,才会立即开启下一个,如:
设置的时间间隔为5秒,执行该任务的时间为10秒
假如第一次线程执行时间为12时10分05秒,则执行结束时间为12时10分15秒,但是在12时10分10秒已经过了5秒,按理应该执行第二次,但是并不会,这是因为在单线程环境中,总是需要等到上一个执行完毕下一个才能执行,所以12时10分10秒的会先被记录,等到第一次执行完毕即12时10分15秒,会进行被执行权争抢,如果顺利拿到被执行权,则会执行12时10分10秒被记录的定时任务,所以如果时间间隔设置太短而执行时间又不确定长,越到后期几乎是一个接一个执行,间隔时间相当于摆设。
但是如果开启多线程模式,如上第一次执行12时10分05秒,执行结束时间应该是12时10分15秒,但是在12时10分10秒时第二次执行时间到了,会开启第二个线程,也就是说会在12时10分10秒到12时10分15秒期间存在两个线程执行同一逻辑,(也就是说用 @Async 注解,使得任务不在 TaskScheduler 线程池中执行,而是每次创建新线程来执行)以此类推。
3. @Scheduled(fixedDelay=2000)
会在上一次定时器结束后按照时间间隔开始执行下一次。
总结
fixedRate 每次任务结束后会从任务编排表中找下一次该执行的任务,判断是否到时机执行。fixedRate 的任务某次执行时间再长也不会造成两次任务实例同时执行,除非用了 @Async 注解。 fixedDelay 总是前一次任务完成后,延时固定长度然后执行一次任务.