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;
}
}