Spring Series---@Scheduled使用深度理解

FeelTouch發表於2018-05-14

功能定位

一種實現程式內定時任務的方法。幾種實現方式類比如下:

1) Java自帶的java.util.Timer類,這個類允許你排程一個java.util.TimerTask任務。 最早的時候就是這樣寫定時任務的。 

2)用java.util.concurrent.ScheduledExecutorService 來實現定時任務,精確的併發語義控制,推薦

3) 開源的第三方框架: Quartz 或者 elastic-job , 但是這個比較複雜和重量級,適用於分散式場景下的定時任務,可以根據需要多例項部署定時任務。 

4) 使用Spring提供的註解: @Schedule  如果定時任務執行時間較短,並且比較單一,可以使用這個註解。

5)建立一個thread,然後讓它在while迴圈裡一直執行著, 通過sleep方法來達到定時任務的效果。這樣可以快速簡單的實現,但是因為執行緒排程受系統和執行緒競爭影響,無法實現可靠精確定時。

使用方法

 實現上主要通過cron和fixedRate兩種方法來實現控制;使用到@Scheduled 和 @EnableScheduled等註解

普通單執行緒

/**
 * Created by Administrator on 2018/5/13.
 */
@SpringBootApplication
@EnableScheduling /**需要新增生命使用定時*/
public class Main {

    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {

        SpringApplication.run(Main.class, args);
        logger.info("start");
    }
}
/**
 * Created by Administrator on 2018/5/13.
 */
@Component
public class ScheduledSingle {

    private static final Logger logger = LoggerFactory.getLogger(ScheduledSingle.class);

    @Scheduled(cron="0/10 * * * * ?")/**間隔10s執行任務*/
    public void executeFileDownLoadTask() {
        Thread current = Thread.currentThread();
        logger.info("Cron任務:"+current.getId()+ ",name:"+current.getName());
    }

    @Scheduled(fixedRate = 1000*5, initialDelay = 1000)
    public void reportCurrentTime(){
        Thread current = Thread.currentThread();
        logger.info("fixedRate任務:"+current.getId()+ ",name:"+current.getName());
    }
}

執行結果


可以看到儘管是兩個任務但仍然由一個執行緒來執行

併發多執行緒

當定時任務很多的時候,為了提高任務執行效率,可以採用並行方式執行定時任務,任務之間互不影響, 

只要實現SchedulingConfigurer介面就可以。

package com.feeler.universe.schedule;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * Created by Administrator on 2018/5/13.
 */
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }

    @Bean(destroyMethod="shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(5);
    }
}

執行結果


並行執行的時候,建立執行緒池採用了newScheduledThreadPool這個執行緒池。 Executors框架中存在幾種執行緒池的建立,一種是 newCachedThreadPool() ,一種是 newFixedThreadPool(), 一種是 newSingleThreadExecutor()

其中newScheduledThreadPool() 執行緒池的採用的佇列是延遲佇列。newScheduledThreadPool() 執行緒池的特性是定時任務能夠定時或者週期性的執行任務。

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
其中執行緒池核心執行緒數是自己設定的,最大執行緒數是最大值。阻塞佇列是自定義的延遲佇列:DelayedWorkQueue()

非同步執行

@SpringBootApplication
public class Application {
 
    public static void main(String[] args) throws Exception {
     
     
     AnnotationConfigApplicationContext rootContext =
     new AnnotationConfigApplicationContext();
 
    rootContext.register(RootContextConfiguration.class);
    rootContext.refresh();
    }
}
@Configuration
@EnableScheduling
@EnableAsync(
mode = AdviceMode.PROXY, proxyTargetClass = false,
order = Ordered.HIGHEST_PRECEDENCE
)
@ComponentScan(
basePackages = "hello"
)
public class RootContextConfiguration implements
AsyncConfigurer, SchedulingConfigurer {
@Bean
public ThreadPoolTaskScheduler taskScheduler()
{
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(20);
scheduler.setThreadNamePrefix("task-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
 
@Override
public Executor getAsyncExecutor()
{
Executor executor = this.taskScheduler();
return executor;
}
 
@Override
public void configureTasks(ScheduledTaskRegistrar registrar)
{
TaskScheduler scheduler = this.taskScheduler();
registrar.setTaskScheduler(scheduler);
}
}

執行原理

spring在初始化bean後,通過“postProcessAfterInitialization”攔截到所有的用到“@Scheduled”註解的方法,並解析相應的的註解引數,放入“定時任務列表”等待後續處理;之後再“定時任務列表”中統一執行相應的定時任務(任務為順序執行,先執行cron,之後再執行fixedRate)。重要步驟。

第一步:依次載入所有的實現Scheduled註解的類方法。第一步:依次載入所有的實現Scheduled註解的類方法。

//說明:ScheduledAnnotationBeanPostProcessor繼承BeanPostProcessor。
@Override
public Object postProcessAfterInitialization(final Object bean, String beanName) {
          //省略多個判斷條件程式碼
         for (Map.Entry<Method, Set<Scheduled>> entry : annotatedMethods.entrySet()) {
            Method method = entry.getKey();
            for (Scheduled scheduled : entry.getValue()) {
               processScheduled(scheduled, method, bean);
            }
         }
   }
   return bean;
}

第二步:將對應型別的定時器放入相應的“定時任務列表”中。

//說明:ScheduledAnnotationBeanPostProcessor繼承BeanPostProcessor。
//獲取scheduled類引數,之後根據引數型別、相應的延時時間、對應的時區放入不同的任務列表中
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {   
     //獲取corn型別
      String cron = scheduled.cron();
      if (StringUtils.hasText(cron)) {
         Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
         processedSchedule = true;
         String zone = scheduled.zone();
         //放入cron任務列表中(不執行)
         this.registrar.addCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)));
      }
      //執行頻率型別(long型別)
      long fixedRate = scheduled.fixedRate();
      String fixedDelayString = scheduled.fixedDelayString();
      if (fixedRate >= 0) {
         Assert.isTrue(!processedSchedule, errorMessage);
         processedSchedule = true;
          //放入FixedRate任務列表中(不執行)(registrar為ScheduledTaskRegistrar)
         this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
      }
     //執行頻率型別(字串型別,不接收引數計算如:600*20)
      String fixedRateString = scheduled.fixedRateString();
      if (StringUtils.hasText(fixedRateString)) {
         Assert.isTrue(!processedSchedule, errorMessage);
         processedSchedule = true;
         if (this.embeddedValueResolver != null) {
            fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
         }
         fixedRate = Long.parseLong(fixedRateString);
         //放入FixedRate任務列表中(不執行)
         this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
      }
}
   return bean;
}
第三步:執行相應的定時任務。

說明:定時任務先執行corn,判斷定時任務的執行時間,計算出相應的下次執行時間,放入執行緒中,到相應的時間後進行執行。之後執行按“頻率”(fixedRate)執行的定時任務,直到所有任務執行結束。

protected void scheduleTasks() {
   //順序執行相應的Cron
   if (this.cronTasks != null) {
      for (CronTask task : this.cronTasks) {
         this.scheduledFutures.add(this.taskScheduler.schedule(
               task.getRunnable(), task.getTrigger()));
      }
   }
  //順序執行所有的“fixedRate”定時任務(無延遲,也就是說initialDelay引數為空),因為無延遲,所以定時任務會直接執行一次,執行任務完成後,會將下次執行任務的時間放入delayedExecute中等待下次執行。
   if (this.fixedRateTasks != null) {
      for (IntervalTask task : this.fixedRateTasks) {
         if (task.getInitialDelay() > 0) {
            Date startTime = new Date(now + task.getInitialDelay());
            this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate(
                  task.getRunnable(), startTime, task.getInterval()));
         }
         else {
            this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate(
                  task.getRunnable(), task.getInterval()));
         }
      }
   }
//順序執行所有的“fixedRate”定時任務(有延遲,也就是說initialDelay引數不為空)
   if (this.fixedDelayTasks != null) {
      for (IntervalTask task : this.fixedDelayTasks) {
         if (task.getInitialDelay() > 0) {
            Date startTime = new Date(now + task.getInitialDelay());
            this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay(
                  task.getRunnable(), startTime, task.getInterval()));
         }
         else {
            this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay(
                  task.getRunnable(), task.getInterval()));
         }
      }
   }
}
接下來看下定時任務run(extends自Runnable介面)方法:


//說明:每次執行定時任務結束後,會先設定下下次定時任務的執行時間,以此來確認下次任務的執行時間。
public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    else if (!periodic)
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime();
        reExecutePeriodic(outerTask);
    }
} 

注意事項

從上面的程式碼可以看出,如果多個定時任務定義的是同一個時間,那麼也是順序執行的,會根據程式載入Scheduled方法的先後來執行。
但是如果某個定時任務執行未完成會出現什麼現象呢? 
答:此任務一直無法執行完成,無法設定下次任務執行時間,之後會導致此任務後面的所有定時任務無法繼續執行,也就會出現所有的定時任務“失效”現象。
所以應用springBoot中定時任務的方法中,一定不要出現“死迴圈”、“http持續等待無響應”現象,否則會導致定時任務程式無法正常。再就是非特殊需求情況下可以把定時任務“分散”下。

相關文章