1. 程式人生 > >Springboot 2.0.* 及低版本定時任務@Scheduled多執行緒配置

Springboot 2.0.* 及低版本定時任務@Scheduled多執行緒配置

1. 首先在springboot啟動類上新增 @EnableScheduling 註解。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class KittyApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(KittyApiApplication.class, args);
    }

}



2. 將任務類註冊到spring容器,然後在其任務方法上新增 @Scheduled 註解,@Scheduled有多個屬性:

 ① cron:cron表示式,指定任務在特定時間執行;

 ② fixedDelay:表示上一次任務執行完成後多久再次執行,引數型別為long,單位ms;

 ③ fixedDelayString:與fixedDelay含義一樣,只是引數型別變為String;

 ④ fixedRate:表示按一定的頻率執行任務,引數型別為long,單位ms;

 ⑤ fixedRateString: 與fixedRate的含義一樣,只是將引數型別變為String;

 ⑥ initialDelay:表示延遲多久後第一次執行任務,引數型別為long,單位ms;

 ⑦ initialDelayString:與initialDelay的含義一樣,只是將引數型別變為String;

 ⑧ zone:時區,預設為當前時區,一般沒有用到。

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

/**
 * @author 
 * @version 2019/4/15 下午 05:00
 */
@Slf4j
@Service
public class TimedTaskServiceImpl {

    @Scheduled(cron = "*/10 * * * * ?")
    public void task01() {
        log.info("task01: " + System.currentTimeMillis());
    }

    @Scheduled(cron = "*/10 * * * * ?")
    public void task02() {
        log.info("task02: " + System.currentTimeMillis());
    }

    @Scheduled(cron = "*/10 * * * * ?")
    public void task03() {
        log.info("task03: " + System.currentTimeMillis());
    }
}



3. springboot 2.0 使用的是spring framework 5.0,在spring 3.0 中就引入了TaskScheduler介面進行非同步執行和任務排程的抽象。spring預設是以單執行緒執行任務排程,想要設定多執行緒在2.0及以前的版本需要實現 SchedulingConfigurer 介面。

(1)springboot 2.1.*以後可以直接通過在properties中通過屬性配置:

# 執行緒池大小 
spring.task.scheduling.pool.size=10 
# 執行緒名字首 
spring.task.scheduling.thread-name-prefix=task-pool-

(2)springboot 2.0及以前就需要實現 SchedulingConfigurer 介面,點到@EnableScheduling註解中,可以看到有SchedulingConfigurer介面、SchedulingTaskRegistrar類和ScheduledAnnotationBeanPostProcessor類等。

SchedulingConfigurer介面內容如下,只有一個抽象方法。

@EnableScheduling 註解的註釋裡也給出了例子,實現這個SchedulingConfigurer介面就可以實現多執行緒定時任務。值得注意的是最後一段,將執行緒池交由spring容器管理,指定銷燬回撥在Bean銷燬時呼叫執行緒池的shutdown方法,保證spring容器關閉前銷燬執行緒池中的執行緒,防止執行緒未終結而駐留。(如果不指定在linux tomcat中可能會使tomcat程序無法通過shutdown命令關閉,導致記憶體洩露)

實現SchedulingConfigurer介面的

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

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

@Configuration
public class ScheduleConfig implements SchedulingConfigurer {

    @Bean(destroyMethod="shutdownNow")
    public ScheduledExecutorService taskExecutors() {
        return Executors.newScheduledThreadPool(10);
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        //引數傳入一個執行緒池
        scheduledTaskRegistrar.setScheduler(taskExecutors());
    }

}

在專案部署到tomcat中,如果按照例子中建立執行緒池需要指定銷燬方法為shutdownNow,shutdown會繼續執行並且完成所有未執行的任務,shutdownNow 會清除所有未執行的任務並且在執行執行緒上呼叫interrupt() 。

ps:博主由於剛開始未設定銷燬方法為shutdownNow,在centos上執行tomcat的shutdown命令無法結束tomcat程序,在銷燬spring容器後執行緒池中執行緒還未被殺死,從而導致tomcat程序一直駐留,錯誤資訊如下:(web應用啟動了一個執行緒,但未能停止它,這很可能造成記憶體洩漏)

16-May-2019 23:19:15.018 INFO [main] org.apache.catalina.core.StandardServer.await A valid shutdown command was received via the shutdown port. Stopping the Server instance.
16-May-2019 23:19:15.019 INFO [main] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8080"]
16-May-2019 23:19:15.070 INFO [main] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["ajp-nio-8009"]
16-May-2019 23:19:15.121 INFO [main] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
16-May-2019 23:19:15.240 WARNING [localhost-startStop-1] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [kittyapi] appears to have started a thread named [pool-2-thread-1] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 sun.misc.Unsafe.park(Native Method)
 java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
 java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
 java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1093)
 java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
 java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
 java.lang.Thread.run(Thread.java:748)

以上是jdk的Executors建立的執行緒池,也可以使用spring的執行緒池ThreadPoolTaskScheduler(ScheduledThreadPoolExecutor的包裝)來建立執行緒池,具體如下:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executor;

@Slf4j
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {

    /**
     * 定時任務執行緒池
     *
     * @return
     */
    @Bean("scheduledThreadPoolExecutor")
    public Executor scheduledThreadPoolExecutor() {
        log.info("start scheduledThreadPoolExecutor");
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        // 配置核心執行緒數
        scheduler.setPoolSize(10);
        return scheduler;
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        // 引數傳入一個執行緒池
        scheduledTaskRegistrar.setScheduler(scheduledThreadPoolExecutor());
    }

}

ThreadPoolTaskScheduler類的繼承和實現關係如下:

ThreadPoolTaskScheduler是TaskScheduler介面的實現類,它的父類ExecutorConfigurationSupport重寫了銷燬方法destroy( )和shutdown( ),通過檢視原始碼可以看到,如果不設定等待任務在關閉容器時完成(waitForTasksToCompleteOnShutdown = true),那麼就預設呼叫了ScheduledThreadPoolExecutor(後續解釋為什麼是這個類)類的shutdownNo方法

ThreadPoolTaskScheduler類是spring對jdk中ScheduledThreadPoolExecutor類的包裝,當建立一個ThreadPoolTaskScheduler物件的bean時,它的內部就已經自動建立了一個預設池大小為1的ScheduledThreadPoolExecutor執行緒池物件,要注意是在spring容器註冊bean時才會去初始化內部的執行緒池。

下面來看ScheduledTaskRegistrar類,可以通過它裡面的方法設定TaskScheduler的子類也就是ThreadPoolTaskScheduler類,所以在實現SchedulingConfigurer介面時可以通過呼叫ThreadPoolTaskScheduler中的set***方法注入外部執行緒池。

最後看看ScheduledAnnotationBeanPostProcessor這個類,它內部例項化了一個ScheduledTaskRegistrar物件。

接著往下看,在完成註冊的方法裡,如果有物件有TaskScheduler或者ScheduledExecutorService物件那麼直接使用該物件,往下走如果滿足條件定時執行緒池為null,那麼就呼叫BeanFactory先使用class型別去獲取容器中的TaskScheduler的子類,如果容器中存在多個TaskScheduler型別的bean,那麼使用預設bean名稱去獲取,預設名稱就是taskScheduler。

接著往下走,可以看到,如果剛開始沒有設定TaskScheduler執行緒池,並且容器中也沒有註冊TaskScheduler執行緒池物件,那麼ScheduledTaskRegistrar會建立一個單執行緒執行緒池來執行定時任務。

總結以上,就會就會發現直接在spring容器中註冊一個TaskScheduler子類也就是ThreadPoolTaskScheduler就可以了(如果存在多個,要指定bean名稱為taskScheduler),就不用去實現SchedulingConfigurer介面了。如果定時任務較多複雜,建議整合Quartz

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Slf4j
@Configuration
public class ExecutorConfig {

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