本文将用测试代码验证:定时任务阻塞问题。
在springboot中使用定时任务的步骤
-
在启动类上加上注解:
@EnableScheduling
,表示允许定时任务执行 -
定时任务需要在类上加上
@Component
或者其衍生类(Controller、Service等),用于纳入Spring容器管理。 -
在需要定时任务方法上增加注解
@Scheduled
,注解的参数是定时任务执行时机
首先需要知道:定时任务默认是单线程的。所以默认情况下,上一个定时任务没有执行完,下一个定时任务是不会开始的。
单线程定时任务
1. 示例1,最简单的例子
// 示例代码:最简单的定时任务
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每秒执行一次
System.out.println("scheduler1 执行: " + Thread.currentThread() + "-" + DateTime.now());
}
如上方法,定时任务是每隔1s触发一次。
2. 示例2,验证定时任务阻塞
但是如果定时任务执行时间超过1s,下一个定时任务会被阻塞,直到上一个定时任务被执行完。
// 示例代码:验证定时任务阻塞问题
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每秒执行一次
System.out.println("scheduler1 执行: " + Thread.currentThread() + "-" + DateTime.now());
try {
Thread.sleep(5*1000); // 5s
} catch (Exception e) {
System.out.println(e.toString());
}
}
可以发现定时任务是每隔6s执行一次(1+5)。
上一个任务没执行完,下一个任务会阻塞,待上一个执行完后,下一个定时任务不是立刻执行,而是需要等待1s(定时任务cron时间)才会执行。可以理解成是上一个任务执行完,才会开始计时。
3. 示例3,验证不同定时任务时的情况
上面这种是一个定时任务,那如果是不同的定时任务呢?
// 示例代码
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每1s执行一次
System.out.println("scheduler1 执行: " + Thread.currentThread() + "-" + DateTime.now());
try {
Thread.sleep(5*1000); // 5s
} catch (Exception e) {
System.out.println(e.toString());
}
}
@Scheduled(cron = "0/2 * * * * ?")
public void test2() {
// 每2s执行一次
System.out.println("scheduler2 执行: " + Thread.currentThread() + "-" + DateTime.now());
}
可以看到,第一个定时任务没执行完时,第二个定时任务也是被阻塞的。而且是同一个线程去执行的这两个定时任务。
多线程定时任务
从上面的例子可以看出,默认情况下,定时任务是单线程的,上一个任务没执行完,下一个任务不会开始。那么如何让不同的线程去执行不同的定时任务呢
4. 示例4,多线程执行任务
那么实际生产环境中,我们肯定是希望每个定时任务方法是独立执行的。换句话说就是他们是在不同的线程中执行的。
只需要实现SchedulingConfigurer
接口,并重写configureTasks
方法即可,注意实现类要加@Configuration
注解,例如
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
Method[] methods = BatchProperties.Job.class.getMethods();
int defaultPoolSize = 20;
int corePoolSize = 0;
if (methods != null && methods.length > 0) {
System.out.println(methods.length);
for (Method method : methods) {
Scheduled annotation = method.getAnnotation(Scheduled.class);
if (annotation != null) {
corePoolSize++;
}
}
if (defaultPoolSize > corePoolSize) {
corePoolSize = defaultPoolSize;
}
}
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(corePoolSize));
}
}
现在来看示例2的代码执行的结果
可以看到还是每隔6s(5+1)执行一次。
那么示例3的代码执行结果又是什么呢
可以看到,定时任务2是每隔2s执行一次的,与第一个定时任务无关。第一个定时任务依然是每隔6s执行一次。而且第一个定时任务和第二个定时任务是不同的线程执行的。
也就是说,不同的任务之间是互不影响的,但是同一个定时任务还是会存在上一个任务执行时间超长,下一个会被阻塞的问题。这种设计其实是有道理的。例如,我需要用定时任务将一批工单推送给第三方,伪代码如下
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每秒执行一次
List<ChainTask> taskList = chainTaskDao.find();
for (ChainTask task : taskList) {
service.push(task); // 工单推送逻辑
task.setIsPush(true);
chainTaskDao.update(task);
}
}
如果第一次任务还没执行完,第二次任务开始了,这时候第二次任务会获取到第一次任务尚未推送的工单,并执行推送。这样有些工单可能就会被重复执行,这种可能是有风险的,所以同一个定时任务会被阻塞,这种设计是有道理的。
5. 示例5,线程池执行任务的情况不会被阻塞
但也不是同一个定时任务就一定会被上一个没执行完的定时任务阻塞,例如定时任务中处理逻辑调用线程池去执行,那么下一个定时任务就不会被阻塞。
@Scheduled(cron = "0/1 * * * * ?")
public void test2() {
// 每秒执行一次
wsAsyncService.print("scheduler2 执行: " + Thread.currentThread() + "-" + DateTime.now());
}
其中wsAsyncService.print()
方法代码如下,尚未用线程池执行示例
// 普通方法示例
// @Async("wsSlAsyncPool")
@Override
public void print(String message) {
System.out.println(message);
try {
Thread.sleep(5*1000);
} catch (Exception e) {
System.out.println(e.toString());
}
}
首先,定时任务是每隔1s执行一次,是调用普通方法来执行的。
调用普通方法执行结果
可以看到,是每隔6s执行一次,也就是下一个定时任务会被上一个没执行完的任务阻塞。
现在来看如果是交给线程池处理呢。
// 交给线程池处理
@Async("wsSlAsyncPool")
@Override
public void print(String message) {
System.out.println(message);
try {
Thread.sleep(5*1000);
} catch (Exception e) {
System.out.println(e.toString());
}
}
执行结果如下
可以看到,任务是每隔1s执行一次,下一个定时任务不会受上一个没执行完的任务阻塞。
总结
- 定时任务默认是单线程的。如果任务执行时间超过定时任务间隔时间,不管是同一个定时任务还是不同的定时任务,下一个任务都会被阻塞。
- 实现
SchedulingConfigurer
接口后,定时任务会变成多线程执行。不同的定时任务之间互不影响,同一个定时任务(方法)依然会有被阻塞的机制。 - 如果定时任务交给线程池处理,则下一个任务不会被阻塞。