前言
最近项目中出现了一个问题,发现自己的定时器任务在线上没有执行,但是在线下测试时却能执行,最后谷歌到了这篇文章SpringBoot踩坑日记-定时任务不定时了?;
本篇文章主要以自己在项目中遇到的问题为背景,并不涉及源码;
Scheduled 定时任务
Scheduled
注解的具体使用方法自行百度或谷歌,这里只是使用其中的一种方式;
验证Scheduled为单线程执行
- 测试代码
@Component public class TestScheduling { private static final Logger log= LoggerFactory.getLogger(TestScheduling.class); @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30) public void testOne() { //打印线程名字 log.info("ThreadName:====one====" + Thread.currentThread().getName()); } @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30) public void testTwo(){ //打印线程名字 log.info("ThreadName:====two====" + Thread.currentThread().getName()); } }
执行结果:
2019-11-13 16:09:07.205 INFO 26976 --- [ scheduling-1] c.example.async.timetask.TestScheduling : ThreadName:====one====scheduling-1 2019-11-13 16:09:07.205 INFO 26976 --- [ scheduling-1] c.example.async.timetask.TestScheduling : ThreadName:====two====scheduling-1 2019-11-13 16:09:37.206 INFO 26976 --- [ scheduling-1] c.example.async.timetask.TestScheduling : ThreadName:====one====scheduling-1 2019-11-13 16:09:37.206 INFO 26976 --- [ scheduling-1] c.example.async.timetask.TestScheduling : ThreadName:====two====scheduling-1
弊端
如若有多个定时器时,当其中某个任务出现死循环或一直等待时,所有的定时任务将不会执行,这就是上面说的本地定时任务能执行,线上运行久了,定时任务就不会执行了;
解决方案
1. 异步注解@Async
在所执行的定时任务的方法(方法为public)上添加@Async
注解,@Async
注解的具体用法自行百度或谷歌,这里使用其中的一种方式。
- 代码
@Component public class TestScheduling { private static final Logger log = LoggerFactory.getLogger(TestScheduling.class); @Async @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30) public void testOne() { //打印线程名字 log.info("ThreadName:====one====" + Thread.currentThread().getName()); } @Async @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30) public void testTwo() { //打印线程名字 log.info("ThreadName:====two====" + Thread.currentThread().getName()); } }
- 结果
2019-11-13 16:59:16.394 INFO 36964 --- [cTaskExecutor-2] c.example.async.timetask.TestScheduling : ThreadName:====one====SimpleAsyncTaskExecutor-2 2019-11-13 16:59:16.394 INFO 36964 --- [cTaskExecutor-1] c.example.async.timetask.TestScheduling : ThreadName:====two====SimpleAsyncTaskExecutor-1 2019-11-13 16:59:46.391 INFO 36964 --- [cTaskExecutor-3] c.example.async.timetask.TestScheduling : ThreadName:====two====SimpleAsyncTaskExecutor-3 2019-11-13 16:59:46.391 INFO 36964 --- [cTaskExecutor-4] c.example.async.timetask.TestScheduling : ThreadName:====one====SimpleAsyncTaskExecutor-4
可以发现,线程虽然不是同一个线程了,但是定时任务每一次执行都是新创建的线程,用完之后就销毁,这样频繁的创建销毁线程是很费资源的;
怎么发现线程被销毁,在Windows系统上可以使用Java自带的jvisualvm.exe
工具来查看应用的运行状况;
2. 使用异步注解+线程池
为什么这么说,百度谷歌了很多文章,都是创建了线程池,最后异步注解却没有用创建的线程池。
- 创建线程池
创建线程池的方式很多种,这里使用其中的一种:
@Configuration public class AsyncConfig { @Bean public AsyncTaskExecutor asyncTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池 executor.setThreadNamePrefix("courses-schedule-"); //最大线程数10:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程 executor.setMaxPoolSize(10); //核心线程数3:线程池创建时候初始化的线程数 executor.setCorePoolSize(3); //缓冲队列0:用来缓冲执行任务的队列 executor.setQueueCapacity(5); //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁 executor.setKeepAliveSeconds(60); // 当线程池已满,且等待队列也满了的时候,直接抛弃当前线程(不会抛出异常) // executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); return executor; } }
这里的线程池名字为:asyncTaskExecutor
,就是方法名;其中线程池里面的属性根据自己的情况而定;
这一步很多文章都写到了,而且创建线程池的方式也各种各样;
- 注解上使用线程池
@Component public class TestScheduling { private static final Logger log = LoggerFactory.getLogger(TestScheduling.class); @Async("asyncTaskExecutor") @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30) public void testOne() { //打印线程名字 log.info("ThreadName:====one====" + Thread.currentThread().getName()); } @Async("asyncTaskExecutor") @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 30) public void testTwo() { //打印线程名字 log.info("ThreadName:====two====" + Thread.currentThread().getName()); } }
注意: 这里在@Async
注解里面添加了线程名字为asyncTaskExecutor
- 执行结果
2019-11-13 17:22:52.021 INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling : ThreadName:====one====courses-schedule-2 2019-11-13 17:22:52.021 INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling : ThreadName:====two====courses-schedule-3 2019-11-13 17:23:22.021 INFO 5448 --- [rses-schedule-1] c.example.async.timetask.TestScheduling : ThreadName:====two====courses-schedule-1 2019-11-13 17:23:22.021 INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling : ThreadName:====one====courses-schedule-2 2019-11-13 17:23:52.021 INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling : ThreadName:====two====courses-schedule-3 2019-11-13 17:23:52.021 INFO 5448 --- [rses-schedule-1] c.example.async.timetask.TestScheduling : ThreadName:====one====courses-schedule-1 2019-11-13 17:24:22.022 INFO 5448 --- [rses-schedule-2] c.example.async.timetask.TestScheduling : ThreadName:====two====courses-schedule-2 2019-11-13 17:24:22.022 INFO 5448 --- [rses-schedule-3] c.example.async.timetask.TestScheduling : ThreadName:====one====courses-schedule-3
调用使用@Async注解的方法
这个主要还是自己基础太差的原因,在调用有@Async
的方法的时候,注意调用该方法的方式,有些方式并不会使@Async
注解产生作用。
比如以下情况:
@Component public class AsyncTest { @Async public void testOne(){ System.out.println("ThreadName:===one===="+Thread.currentThread().getName()); } } @Service public class AsyncServiceImpl implements AsyncService { @Autowired private AsyncTest asyncTest; @Override public void testAsync() { // 步骤一 asyncTest.testOne(); // 步骤二 testOne(); } @Async public void testOne() { System.out.println("ThreadName:===AsyncServiceImpl+testOne====" + Thread.currentThread().getName()); } }
这里注意步骤一和二,两个方法都是用了@Async
注解的,但是步骤二就是方法调用,并不是异步调用;
总结
经验是不断积累的,需要学习的地方还有很多,继续努力;
参考: