本文將用測試代碼驗(yàn)證:定時任務(wù)阻塞問題强霎。
在springboot中使用定時任務(wù)的步驟
1.在啟動類上加上注解:@EnableScheduling城舞,表示允許定時任務(wù)執(zhí)行
2.定時任務(wù)需要在類上加上@Component或者其衍生類(Controller、Service等)脱柱,用于納入Spring容器管理拉馋。
3.在需要定時任務(wù)方法上增加注解@Scheduled,注解的參數(shù)是定時任務(wù)執(zhí)行時機(jī)
首先需要知道:定時任務(wù)默認(rèn)是單線程的日川。所以默認(rèn)情況下矩乐,上一個定時任務(wù)沒有執(zhí)行完散罕,下一個定時任務(wù)是不會開始的。
單線程定時任務(wù)
1. 示例1卿樱,最簡單的例子
// 示例代碼:最簡單的定時任務(wù)
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每秒執(zhí)行一次
System.out.println("scheduler1 執(zhí)行: " + Thread.currentThread() + "-" + DateTime.now());
}
如上方法硫椰,定時任務(wù)是每隔1s觸發(fā)一次。
2. 示例2蹄胰,驗(yàn)證定時任務(wù)阻塞
但是如果定時任務(wù)執(zhí)行時間超過1s奕翔,下一個定時任務(wù)會被阻塞派继,直到上一個定時任務(wù)被執(zhí)行完。
// 示例代碼:驗(yàn)證定時任務(wù)阻塞問題
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每秒執(zhí)行一次
System.out.println("scheduler1 執(zhí)行: " + Thread.currentThread() + "-" + DateTime.now());
try {
Thread.sleep(5*1000); // 5s
} catch (Exception e) {
System.out.println(e.toString());
}
}
可以發(fā)現(xiàn)定時任務(wù)是每隔6s執(zhí)行一次(1+5)。
上一個任務(wù)沒執(zhí)行完绅络,下一個任務(wù)會阻塞恩急,待上一個執(zhí)行完后,下一個定時任務(wù)不是立刻執(zhí)行此叠,而是需要等待1s(定時任務(wù)cron時間)才會執(zhí)行拌蜘⊙览觯可以理解成是上一個任務(wù)執(zhí)行完烤芦,才會開始計時。
3. 示例3铜涉,驗(yàn)證不同定時任務(wù)時的情況
上面這種是一個定時任務(wù)遂唧,那如果是不同的定時任務(wù)呢盖彭?
// 示例代碼
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每1s執(zhí)行一次
System.out.println("scheduler1 執(zhí)行: " + 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執(zhí)行一次
System.out.println("scheduler2 執(zhí)行: " + Thread.currentThread() + "-" + DateTime.now());
}
可以看到召边,第一個定時任務(wù)沒執(zhí)行完時,第二個定時任務(wù)也是被阻塞的片挂。而且是同一個線程去執(zhí)行的這兩個定時任務(wù)音念。
多線程定時任務(wù)
從上面的例子可以看出躏敢,默認(rèn)情況下父丰,定時任務(wù)是單線程的,上一個任務(wù)沒執(zhí)行完蛾扇,下一個任務(wù)不會開始攘烛。那么如何讓不同的線程去執(zhí)行不同的定時任務(wù)呢
4. 示例4,多線程執(zhí)行任務(wù)
那么實(shí)際生產(chǎn)環(huán)境中镀首,我們肯定是希望每個定時任務(wù)方法是獨(dú)立執(zhí)行的坟漱。換句話說就是他們是在不同的線程中執(zhí)行的。
只需要實(shí)現(xiàn)SchedulingConfigurer接口更哄,并重寫configureTasks方法即可芋齿,注意實(shí)現(xiàn)類要加@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));
}
}
現(xiàn)在來看示例2的代碼執(zhí)行的結(jié)果
可以看到還是每隔6s(5+1)執(zhí)行一次。
那么示例3的代碼執(zhí)行結(jié)果又是什么呢
可以看到赦役,定時任務(wù)2是每隔2s執(zhí)行一次的,與第一個定時任務(wù)無關(guān)栅炒。第一個定時任務(wù)依然是每隔6s執(zhí)行一次掂摔。而且第一個定時任務(wù)和第二個定時任務(wù)是不同的線程執(zhí)行的。
也就是說赢赊,不同的任務(wù)之間是互不影響的乙漓,但是同一個定時任務(wù)還是會存在上一個任務(wù)執(zhí)行時間超長,下一個會被阻塞的問題释移。這種設(shè)計其實(shí)是有道理的叭披。例如,我需要用定時任務(wù)將一批工單推送給第三方玩讳,偽代碼如下
@Scheduled(cron = "0/1 * * * * ?")
public void test1() {
// 每秒執(zhí)行一次
List<ChainTask> taskList = chainTaskDao.find();
for (ChainTask task : taskList) {
service.push(task); // 工單推送邏輯
task.setIsPush(true);
chainTaskDao.update(task);
}
}
如果第一次任務(wù)還沒執(zhí)行完涩蜘,第二次任務(wù)開始了,這時候第二次任務(wù)會獲取到第一次任務(wù)尚未推送的工單锋边,并執(zhí)行推送皱坛。這樣有些工單可能就會被重復(fù)執(zhí)行,這種可能是有風(fēng)險的豆巨,所以同一個定時任務(wù)會被阻塞剩辟,這種設(shè)計是有道理的。
5. 示例5往扔,線程池執(zhí)行任務(wù)的情況不會被阻塞
但也不是同一個定時任務(wù)就一定會被上一個沒執(zhí)行完的定時任務(wù)阻塞贩猎,例如定時任務(wù)中處理邏輯調(diào)用線程池去執(zhí)行,那么下一個定時任務(wù)就不會被阻塞萍膛。
@Scheduled(cron = "0/1 * * * * ?")
public void test2() {
// 每秒執(zhí)行一次
wsAsyncService.print("scheduler2 執(zhí)行: " + Thread.currentThread() + "-" + DateTime.now());
}
其中wsAsyncService.print()方法代碼如下吭服,尚未用線程池執(zhí)行示例
// 普通方法示例
// @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());
}
}
首先,定時任務(wù)是每隔1s執(zhí)行一次蝗罗,是調(diào)用普通方法來執(zhí)行的艇棕。
調(diào)用普通方法執(zhí)行結(jié)果
可以看到,是每隔6s執(zhí)行一次串塑,也就是下一個定時任務(wù)會被上一個沒執(zhí)行完的任務(wù)阻塞沼琉。
現(xiàn)在來看如果是交給線程池處理呢带斑。
// 交給線程池處理
@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());
}
}
執(zhí)行結(jié)果如下
可以看到矮男,任務(wù)是每隔1s執(zhí)行一次,下一個定時任務(wù)不會受上一個沒執(zhí)行完的任務(wù)阻塞酗昼。
總結(jié)
1.定時任務(wù)默認(rèn)是單線程的。如果任務(wù)執(zhí)行時間超過定時任務(wù)間隔時間闺骚,不管是同一個定時任務(wù)還是不同的定時任務(wù)彩扔,下一個任務(wù)都會被阻塞。
2.實(shí)現(xiàn)SchedulingConfigurer接口后僻爽,定時任務(wù)會變成多線程執(zhí)行虫碉。不同的定時任務(wù)之間互不影響,同一個定時任務(wù)(方法)依然會有被阻塞的機(jī)制进泼。
3.如果定時任務(wù)交給線程池處理蔗衡,則下一個任務(wù)不會被阻塞纤虽。