Springboot 2.0.* 及低版本定時任務@Scheduled多線程配置

1. 首先在springboot啓動類上添加 @EnableScheduling 註解。java

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有多個屬性:linux

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

 ② fixedDelay:表示上一次任務執行完成後多久再次執行,參數類型爲long,單位ms;spring

 ③ fixedDelayString:與fixedDelay含義同樣,只是參數類型變爲String;apache

 ④ fixedRate:表示按必定的頻率執行任務,參數類型爲long,單位ms;centos

 ⑤ fixedRateString: 與fixedRate的含義同樣,只是將參數類型變爲String;api

 ⑥ initialDelay:表示延遲多久後第一次執行任務,參數類型爲long,單位ms;tomcat

 ⑦ initialDelayString:與initialDelay的含義同樣,只是將參數類型變爲String;springboot

 ⑧ 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.1及之前的版本。

(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接口的configureTasks()。

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進程,tomcat結束線程在結束spring容器時,沒法結束線程池中線程,線程池中線程在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,spring默認使用此名稱的bean,這裏只是設置了線程數量,其餘屬性可根據具體需求設置),就不用去實現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;
    }
}
相關文章
相關標籤/搜索