springboot定時(shí)任務(wù)@Scheduled執(zhí)行多次的問(wèn)題
springboot定時(shí)任務(wù)@Scheduled執(zhí)行多次
在spring boot開(kāi)發(fā)定時(shí)任務(wù)時(shí)遇到一個(gè)很怪異的現(xiàn)象..我進(jìn)行調(diào)試模式,在沒(méi)有bug的情況下.執(zhí)行了三 次才停止..如圖:

原因
是因?yàn)閳?zhí)行時(shí)間太短,在CronSequenceGenerator.class的next方法。
public Date next(Date date) {
Calendar calendar = new GregorianCalendar();
calendar.setTimeZone(this.timeZone);
calendar.setTime(date);
//1.設(shè)置下次執(zhí)行時(shí)間的毫秒為0,如上次任務(wù)執(zhí)行過(guò)程不足1秒,則calendar的時(shí)間會(huì)被設(shè)置成上次任務(wù)的執(zhí)行時(shí)間
calendar.set(14, 0);
long originalTimestamp = calendar.getTimeInMillis();
this.doNext(calendar, calendar.get(1));
//2.由于有上面一步,執(zhí)行時(shí)間太短,會(huì)導(dǎo)致下述條件為true
if(calendar.getTimeInMillis() == originalTimestamp) {
//3.calendar在原來(lái)的時(shí)間上增加1秒
calendar.add(13, 1);
//CronSequenceGenerator的doNext算法從指定時(shí)間開(kāi)始(包括指定時(shí)間)查找符合cron表達(dá)式規(guī)則下一個(gè)匹配的時(shí)間
//注意第一個(gè)匹配符是*,由于增加了1秒,依然符合cron="* 0/2 * * * *",所以下一個(gè)執(zhí)行時(shí)間就是在原來(lái)的基礎(chǔ)上增加了一 秒
this.doNext(calendar, calendar.get(1));
}
return calendar.getTime();
代碼會(huì)進(jìn)入if語(yǔ)句,并設(shè)置執(zhí)行時(shí)間在原來(lái)的基礎(chǔ)上增加一秒。
但由于增加一秒后的時(shí)間戳依然符合cron表達(dá)式,于是在執(zhí)行完代碼后一秒,任務(wù)又開(kāi)始執(zhí)行了
解決方法
程序執(zhí)行時(shí)間太短沒(méi)有關(guān)系,只要cron表達(dá)式秒的匹配符不設(shè)置為*就可以了。如圖:

使用 @Scheduled 定時(shí)任務(wù)突然不執(zhí)行了
在 SpringBoot 中可以通過(guò) @Scheduled 注解來(lái)定義一個(gè)定時(shí)任務(wù), 但是有時(shí)候你可能會(huì)發(fā)現(xiàn)有的定時(shí)任務(wù)到時(shí)間了卻沒(méi)有執(zhí)行,但是又不是每次都不執(zhí)行,這是怎么回事?
下面這段代碼定義了一個(gè)每隔十秒鐘執(zhí)行一次的定時(shí)任務(wù):
@Component
public class ScheduledTaskDemo {
private static final Logger logger = LoggerFactory.getLogger(ScheduledTaskDemo.class);
@Scheduled(cron = "0/10 * * * * *")
public void execute() {
logger.info("Scheduled task is running... ...");
}
}
此時(shí)啟動(dòng) SpringBoot 應(yīng)用, 可以在控制臺(tái)看到這個(gè)定時(shí)任務(wù)每隔10秒鐘打印一條log

但是, 一切還沒(méi)結(jié)束,如果沒(méi)有相關(guān)log顯示, 檢查是否在入口類或者 Configuration 類上添加了@EnableScheduling 注解
在上面的相關(guān)代碼中, 我們使用cron表達(dá)式來(lái)指定定時(shí)任務(wù)的執(zhí)行時(shí)間點(diǎn), 即從0秒開(kāi)始, 每隔10秒鐘執(zhí)行一次, 現(xiàn)在我們?cè)偌右粋€(gè)定時(shí)任務(wù):
@Component
public class SecondScheduledTaskDemo {
private static final Logger logger = LoggerFactory.getLogger(ScheduledTaskDemo.class);
@Scheduled(cron = "0/10 * * * * *")
public void second() {
logger.info("Second scheduled task is starting... ...");
logger.info("Second scheduled task is ending... ...");
}
}
現(xiàn)在再啟動(dòng)SpringBoot應(yīng)用, 再看log:

注意log中定時(shí)任務(wù)執(zhí)行的時(shí)間點(diǎn), 第二個(gè)定時(shí)任務(wù)原本應(yīng)該每隔10秒鐘執(zhí)行一次, 但是從23:12:20到23:13:55, 本該執(zhí)行4次, 確只執(zhí)行了2次.
難道是cron表達(dá)式不對(duì)?
No.
為了找到原因, 我們從 @Scheduled 注解的源碼開(kāi)始找:
*
* <p>Processing of {@code @Scheduled} annotations is performed by
* registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be
* done manually or, more conveniently, through the {@code <task:annotation-driven/>}
* element or @{@link EnableScheduling} annotation.
*
劃重點(diǎn), 每一個(gè)有 @Scheduled 注解的方法都會(huì)被注冊(cè)為一個(gè)ScheduledAnnotationBeanPostProcessor, 再接著往下看ScheduledAnnotationBeanPostProcessor:
/**
* Set the {@link org.springframework.scheduling.TaskScheduler} that will invoke
* the scheduled methods, or a {@link java.util.concurrent.ScheduledExecutorService}
* to be wrapped as a TaskScheduler.
* <p>If not specified, default scheduler resolution will apply: searching for a
* unique {@link TaskScheduler} bean in the context, or for a {@link TaskScheduler}
* bean named "taskScheduler" otherwise; the same lookup will also be performed for
* a {@link ScheduledExecutorService} bean. If neither of the two is resolvable,
* a local single-threaded default scheduler will be created within the registrar.
* @see #DEFAULT_TASK_SCHEDULER_BEAN_NAME
*/
public void setScheduler(Object scheduler) {
this.scheduler = scheduler;
}
重點(diǎn)來(lái)了, 注意這句話:

這句話意味著, 如果我們不主動(dòng)配置我們需要的 TaskScheduler, SpringBoot 會(huì)默認(rèn)使用一個(gè)單線程的scheduler來(lái)處理我們用 @Scheduled 注解實(shí)現(xiàn)的定時(shí)任務(wù), 到此我們剛才的問(wèn)題就可以理解了:
23:12:20, 第一個(gè)定時(shí)任務(wù)在線程pool-1-thread-1開(kāi)始執(zhí)行, 由于我們沒(méi)有配置scheduler, 目前這個(gè)線程池pool-1里只有一個(gè)線程, 在打印了starting日志之后, 這個(gè)線程開(kāi)始sleep;第二個(gè)定時(shí)任務(wù)也準(zhǔn)備執(zhí)行, 但是線程池已經(jīng)沒(méi)有多余線程了, 只能等待.
23:12:30, 第一個(gè)定時(shí)任務(wù)還在sleep, 第二個(gè)定時(shí)任務(wù)還在等待.
23:12:35, 第一個(gè)定時(shí)任務(wù)sleep結(jié)束, 打印ending日志并結(jié)束, 此時(shí)線程池空閑, 第二個(gè)定時(shí)任務(wù)從等待狀態(tài)直接開(kāi)始執(zhí)行, 執(zhí)行結(jié)束之后, 線程池空閑.
23:12:40, 線程池空閑, 第一個(gè)定時(shí)任務(wù)執(zhí)行, 打印starting日志, 開(kāi)始sleep.
![]()
搞清楚這個(gè)流程之后, 解決這個(gè)問(wèn)題就很簡(jiǎn)單了.
根據(jù)剛才注釋的描述, 我們只需要提供一個(gè)滿足我們需要的 TaskScheduler 并注冊(cè)到context中就可以了.
@Configuration
public class ScheduledTaskConfiguration implements SchedulingConfigurer {
/**
* Callback allowing a {@link TaskScheduler
* TaskScheduler} and specific {@link Task Task}
* instances to be registered against the given the {@link ScheduledTaskRegistrar}
*
* @param taskRegistrar the registrar to be configured.
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
final ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(2);
taskScheduler.initialize();
taskRegistrar.setTaskScheduler(taskScheduler);
}
}
上面的代碼提供了一個(gè)線程池大小為2的taskScheduler, 現(xiàn)在再啟動(dòng)下SpringBoot看看效果.

可以看到, 當(dāng)線程池里有兩個(gè)線程的時(shí)候, 這兩個(gè)定時(shí)任務(wù)各自按照預(yù)定的時(shí)間進(jìn)行觸發(fā), 互不影響了.
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- SpringBoot使用@Scheduled實(shí)現(xiàn)定時(shí)任務(wù)的并行執(zhí)行
- SpringBoot中實(shí)現(xiàn)@Scheduled動(dòng)態(tài)定時(shí)任務(wù)
- SpringBoot中定時(shí)任務(wù)@Scheduled的多線程使用詳解
- SpringBoot通過(guò)@Scheduled實(shí)現(xiàn)定時(shí)任務(wù)及單線程運(yùn)行問(wèn)題解決
- SpringBoot中定時(shí)任務(wù)@Scheduled注解的使用解讀
- springboot實(shí)現(xiàn)定時(shí)任務(wù)@Scheduled方式
相關(guān)文章
SpringBoot自定義注解使用讀寫(xiě)分離Mysql數(shù)據(jù)庫(kù)的實(shí)例教程
這篇文章主要給大家介紹了關(guān)于SpringBoot自定義注解使用讀寫(xiě)分離Mysql數(shù)據(jù)庫(kù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
Java實(shí)現(xiàn)雙向鏈表(兩個(gè)版本)
這篇文章主要介紹了Java實(shí)現(xiàn)雙向鏈表(兩個(gè)版本)的相關(guān)資料,需要的朋友可以參考下2016-02-02
SpringBoot接收接口入?yún)⒌姆绞叫〗Y(jié)
這篇文章主要給大家介紹了SpringBoot接收接口入?yún)⒌膸追N方式,我們從調(diào)用方的視角去看待這個(gè)問(wèn)題,對(duì)調(diào)用方來(lái)說(shuō),它在調(diào)用接口時(shí)有好幾種傳參方式,下面,將會(huì)依次對(duì)這幾種參數(shù)方式進(jìn)行講解和代碼示例,需要的朋友可以參考下2024-01-01
Spring MVC文件上傳大小和類型限制以及超大文件上傳bug問(wèn)題
這篇文章主要介紹了Spring MVC文件上傳大小和類型限制以及超大文件上傳bug問(wèn)題,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-10-10
linux環(huán)境下java程序打包成簡(jiǎn)單的hello world輸出jar包示例
這篇文章主要介紹了linux環(huán)境下java程序打包成簡(jiǎn)單的hello world輸出jar包,結(jié)合簡(jiǎn)單hello world輸出程序示例分析了Linux環(huán)境下的java可執(zhí)行jar包文件的生成相關(guān)操作技巧,需要的朋友可以參考下2019-11-11
下載遠(yuǎn)程maven倉(cāng)庫(kù)的jar?手動(dòng)放到本地倉(cāng)庫(kù)詳細(xì)操作
這篇文章主要介紹了如何下載遠(yuǎn)程maven倉(cāng)庫(kù)的jar?手動(dòng)放到本地倉(cāng)庫(kù),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
springboot自定義校驗(yàn)注解的實(shí)現(xiàn)過(guò)程
這篇文章主要介紹了springboot自定義校驗(yàn)注解的實(shí)現(xiàn)過(guò)程,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-11-11
解決idea出現(xiàn)的java.lang.OutOfMemoryError:?Java?heap?space的問(wèn)題
我們?cè)谑褂胕dea的時(shí)候經(jīng)常會(huì)遇到一些問(wèn)題,本文介紹了如何解決idea出現(xiàn)的java.lang.OutOfMemoryError:?Java?heap?space的問(wèn)題,文中有相關(guān)的圖文示例,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06

