前言
最近项目中出现了一个问题,发现自己的定时器任务在线上没有执行,但是在线下测试时却能执行,最后谷歌到了这篇文章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
注解的,但是步骤二就是方法调用,并不是异步调用;
总结
经验是不断积累的,需要学习的地方还有很多,继续努力;
参考: