解決使用@Scheduled建立任務時沒法在同一時間執行多個任務的BUG

       最近在項目中須要任務調度框架,正好springboot集成了一個簡單定時調度,並且咱們項目功能比較簡單就不必引入Quartz這種比較大型的框架。可是在使用的過程當中測試人員發現若是多個任務設計同一時間執行會出現只有一個任務在執行其它任務都沒法執行的狀況。由於問題比較嚴重就專門研究了一翻,發現問題還真存在。如下是測試流程:html

1.新建個測試類,裏面定義2個方法execute1和execute2。兩個方法裏面沒有任何操做只打印當前時間和線程名,爲了模擬線上的狀況讓該方法運行的時候sleep 1秒再結束(業務操做須要耗費必定的時間)。java

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class MyScheduled {

    @Scheduled(cron = "0/5 * * * * ?")
    public void execute1(){
        String curName = Thread.currentThread().getName() ;
        System.out.println("當前時間:"+LocalDateTime.now()+"  任務execute1對應的線程名: "+curName);
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Scheduled(cron = "0/5 * * * * ?")
    public void execute2(){

        String curName = Thread.currentThread().getName() ;
        System.out.println("當前時間:"+LocalDateTime.now()+"  任務execute2對應的線程名: "+curName);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

*由於項目使用的是springboot框架爲了讓定時任務生效須要在類上面加上@EnableScheduling以開啓對定時任務的支持mysql

按正常的理解此時運行exect1和execute2打印的線程名應該不一致纔對,可是測試的結果讓人大跌眼鏡。如下是本地的運行結果:spring

當前時間:2018-08-26T10:53:40.123  任務execute1對應的線程名: pool-1-thread-1
當前時間:2018-08-26T10:53:41.127  任務execute2對應的線程名: pool-1-thread-1
當前時間:2018-08-26T10:53:45.014  任務execute2對應的線程名: pool-1-thread-1
當前時間:2018-08-26T10:53:46.028  任務execute1對應的線程名: pool-1-thread-1
當前時間:2018-08-26T10:53:50.016  任務execute2對應的線程名: pool-1-thread-1
當前時間:2018-08-26T10:53:51.029  任務execute1對應的線程名: pool-1-thread-1

能夠發現正如測試同事說的那樣,同一時間間隔的2個定時任務(都設置了5秒運行一次)只會運行一個,而且神奇的一點時線程名字是同樣的。所以有理由懷疑springboot建立線程的時使用了newSingleThreadExecutor。帶着這個疑問,咱們只能一步步來debug了,咱們首先在execute1方法中打個斷點看下調用類和線程池看下是什麼狀況。sql

經過上圖咱們能夠發現springboot建立的線程池poolSize確實是1,當前活動線程數(activethreads)爲1。咱們繼續往下跟蹤,首先咱們從@EnableScheduling這個註解開始跟蹤。api

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}

能夠看到EnableScheduling是將SchedulingConfiguration這個類實例化並注入到springboot容器中,咱們繼續跟蹤下去看下改配置類執行什麼操做。springboot

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.scheduling.annotation;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;

@Configuration
@Role(2)
public class SchedulingConfiguration {
    public SchedulingConfiguration() {
    }

    @Bean(
        name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
    )
    @Role(2)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

能夠看出裏面注入了org.springframework.context.annotation.internalScheduledAnnotationProcessor這個類(*這裏發現一個@Role(2)註解,沒找到這個註解到底實現什麼功能)框架

而後在springboot的API中查找下這個類到底實現什麼功能http://fanyi.baidu.com/transpage?from=auto&to=zh&query=https%3A%2F%2Fdocs.spring.io%2Fspring-framework%2Fdocs%2Fcurrent%2Fjavadoc-api%2Forg%2Fspringframework%2Fscheduling%2Fannotation%2FScheduledAnnotationBeanPostProcessor.html&source=url&ie=utf8&render=1&aldtype=16047(英文很差這裏只好使用百度翻譯一下)測試

大概的意思是若是沒有指定TaskScheduler則會建立一個單線程的默認調度器。所以問題就清楚了,須要本身建立一個TaskScheduler。立馬百度一下,發現很簡單url

@Bean
public TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setPoolSize(50);
    return taskScheduler;
}

只須要把這一段代碼放進啓動類便可。我這邊爲了方便就直接放進MySchedule類裏面,修改後的代碼爲:

import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
@EnableScheduling
public class MyScheduled {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(50);
        return taskScheduler;
    }

    @Scheduled(cron = "0/5 * * * * ?")
    public void execute1(){
        String curName = Thread.currentThread().getName() ;
        System.out.println("當前時間:"+LocalDateTime.now()+"  任務execute1對應的線程名: "+curName);
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Scheduled(cron = "0/5 * * * * ?")
    public void execute2(){

        String curName = Thread.currentThread().getName() ;
        System.out.println("當前時間:"+LocalDateTime.now()+"  任務execute2對應的線程名: "+curName);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

咱們再次測試下效果:

當前時間:2018-08-26T12:09:50.010  任務execute2對應的線程名: taskScheduler-1
當前時間:2018-08-26T12:09:50.010  任務execute1對應的線程名: taskScheduler-2
當前時間:2018-08-26T12:09:55.001  任務execute1對應的線程名: taskScheduler-3
當前時間:2018-08-26T12:09:55.001  任務execute2對應的線程名: taskScheduler-1
當前時間:2018-08-26T12:10:00.017  任務execute2對應的線程名: taskScheduler-2
當前時間:2018-08-26T12:10:00.018  任務execute1對應的線程名: taskScheduler-4
當前時間:2018-08-26T12:10:05.001  任務execute1對應的線程名: taskScheduler-3
當前時間:2018-08-26T12:10:05.001  任務execute2對應的線程名: taskScheduler-1

發現線程名變了,所以問題獲得了完美解決。後來繼續debug發現ScheduledTaskRegistrar 裏面有這麼一行代碼

能夠發現當taskScheduler對象爲空時默認建立的是newSingleThreadScheduledExecutor()  至此問題解決。可是還有如下2個問題後續有空須要繼續研究

1.springboot爲啥默認建立的是newSingleThreadScheduledExecutor?

2.@Role註解到底有什麼用處

 

       通過這一折騰,感受本身對springboot的理解還不夠深刻,但願在項目中一邊踩坑一邊研究。後期打算集成quartz+mysql+ZK實現高可用的任務調度,固然這是後話目前仍是先把項目作好!

相關文章
相關標籤/搜索