Spring使用ThreadPoolTaskExecutor自定義線程池及實現異步調用

  • 2019 年 10 月 14 日
  • 筆記

多線程一直是工作或面試過程中的高頻知識點,今天給大家分享一下使用 ThreadPoolTaskExecutor 來自定義線程池和實現異步調用多線程。

一、ThreadPoolTaskExecutor

本文採用 Executors 的工廠方法進行配置。

1、將線程池用到的參數定義到配置文件中

在項目的 resources 目錄下創建 executor.properties 文件,並添加如下配置:

# 異步線程配置  # 核心線程數  async.executor.thread.core_pool_size=5  # 最大線程數  async.executor.thread.max_pool_size=8  # 任務隊列大小  async.executor.thread.queue_capacity=2  # 線程池中線程的名稱前綴  async.executor.thread.name.prefix=async-service-  # 緩衝隊列中線程的空閑時間  async.executor.thread.keep_alive_seconds=100

2、Executors 的工廠配置

2.1、配置詳情
@Configuration  // @PropertySource是找的target目錄下classes目錄下的文件,resources目錄下的文件編譯後會生成在classes目錄  @PropertySource(value = {"classpath:executor.properties"}, ignoreResourceNotFound=false, encoding="UTF-8")  @Slf4j  public class ExecutorConfig {        @Value("${async.executor.thread.core_pool_size}")      private int corePoolSize;      @Value("${async.executor.thread.max_pool_size}")      private int maxPoolSize;      @Value("${async.executor.thread.queue_capacity}")      private int queueCapacity;      @Value("${async.executor.thread.name.prefix}")      private String namePrefix;      @Value("${async.executor.thread.keep_alive_seconds}")      private int keepAliveSeconds;        @Bean(name = "asyncTaskExecutor")      public ThreadPoolTaskExecutor taskExecutor() {          log.info("啟動");          ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();          // 核心線程數          executor.setCorePoolSize(corePoolSize);          // 最大線程數          executor.setMaxPoolSize(maxPoolSize);          // 任務隊列大小          executor.setQueueCapacity(queueCapacity);          // 線程前綴名          executor.setThreadNamePrefix(namePrefix);          // 線程的空閑時間          executor.setKeepAliveSeconds(keepAliveSeconds);          // 拒絕策略          executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());          // 線程初始化          executor.initialize();          return executor;      }  }
2.2、註解說明
  • @Configuration:Spring 容器在啟動時,會加載帶有 @Configuration 註解的類,對其中帶有 @Bean 註解的方法進行處理。
  • @Bean:是一個方法級別上的註解,主要用在 @Configuration 註解的類里,也可以用在 @Component 註解的類里。添加的 bean 的 id 為方法名。
  • @PropertySource:加載指定的配置文件。value 值為要加載的配置文件,ignoreResourceNotFound 意思是如果加載的文件找不到,程序是否忽略它。默認為 false 。如果為 true ,則代表加載的配置文件不存在,程序不報錯。在實際項目開發中,最好設置為 false 。如果 application.properties 文件中的屬性與自定義配置文件中的屬性重複,則自定義配置文件中的屬性值被覆蓋,加載的是 application.properties 文件中的配置屬性。
  • @Slf4j:lombok 的日誌輸出工具,加上此註解後,可直接調用 log 輸出各個級別的日誌。
  • @Value:調用配置文件中的屬性並給屬性賦予值。
2.3、線程池配置說明
  • 核心線程數:線程池創建時候初始化的線程數。當線程數超過核心線程數,則超過的線程則進入任務隊列。
  • 最大線程數:只有在任務隊列滿了之後才會申請超過核心線程數的線程。不能小於核心線程數。
  • 任務隊列:線程數大於核心線程數的部分進入任務隊列。如果任務隊列足夠大,超出核心線程數的線程不會被創建,它會等待核心線程執行完它們自己的任務後再執行任務隊列的任務,而不會再額外地創建線程。舉例:如果有20個任務要執行,核心線程數:10,最大線程數:20,任務隊列大小:2。則系統會創建18個線程。這18個線程有執行完任務的,再執行任務隊列中的任務。

  • 線程的空閑時間:當 線程池中的線程數量 大於 核心線程數 時,如果某線程空閑時間超過 keepAliveTime ,線程將被終止。這樣,線程池可以動態的調整池中的線程數。
  • 拒絕策略:如果(總任務數 – 核心線程數 – 任務隊列數)-(最大線程數 – 核心線程數)> 0 的話,則會出現線程拒絕。舉例:( 12 – 5 – 2 ) – ( 8 – 5 ) > 0,會出現線程拒絕。線程拒絕又分為 4 種策略,分別為:
    • CallerRunsPolicy():交由調用方線程運行,比如 main 線程。
    • AbortPolicy():直接拋出異常。
    • DiscardPolicy():直接丟棄。
    • DiscardOldestPolicy():丟棄隊列中最老的任務。
2.4、線程池配置個人理解
  • 當一個任務被提交到線程池時,首先查看線程池的核心線程是否都在執行任務。如果沒有,則選擇一條線程執行任務。
  • 如果都在執行任務,查看任務隊列是否已滿。如果不滿,則將任務存儲在任務隊列中。核心線程執行完自己的任務後,會再處理任務隊列中的任務。
  • 如果任務隊列已滿,查看線程池(最大線程數控制)是否已滿。如果不滿,則創建一條線程去執行任務。如果滿了,就按照策略處理無法執行的任務。

二、異步調用線程

通常 ThreadPoolTaskExecutor 是和 @Async 一起使用。在一個方法上添加 @Async 註解,表明是異步調用方法函數。@Async 後面加上線程池的方法名或 bean 名稱,表明異步線程會加載線程池的配置。

@Component  @Slf4j  public class ThreadTest {      /**       * 每10秒循環一次,一個線程共循環10次。       */      @Async("asyncTaskExecutor")      public void ceshi3() {          for (int i = 0; i <= 10; i++) {              log.info("ceshi3: " + i);              try {                  Thread.sleep(2000 * 5);              } catch (InterruptedException e) {                  e.printStackTrace();              }          }      }  }

備註:一定要在啟動類上添加 @EnableAsync 註解,這樣 @Async 註解才會生效。

三、多線程使用場景

1、定時任務 @Scheduled

// 在啟動類上添加 @EnableScheduling 註解  @SpringBootApplication  @EnableScheduling  public class SpringBootStudyApplication {     public static void main(String[] args) {        SpringApplication.run(SpringBootStudyApplication.class, args);     }  }
// @Component 註解將定時任務類納入 spring bean 管理。  @Component  public class listennerTest3 {        @Autowired      private ThreadTest t;        // 每1分鐘執行一次ceshi3()方法      @Scheduled(cron = "0 0/1 * * * ?")      public void run() {          t.ceshi3();      }  }

ceshi3() 方法調用線程池配置,且異步執行。

@Component  @Slf4j  public class ThreadTest {      /**       * 每10秒循環一次,一個線程共循環10次。       */      @Async("asyncTaskExecutor")      public void ceshi3() {          for (int i = 0; i <= 10; i++) {              log.info("ceshi3: " + i);              try {                  Thread.sleep(2000 * 5);              } catch (InterruptedException e) {                  e.printStackTrace();              }          }      }  }

2、程序一啟動就異步執行多線程

通過繼承 CommandLineRunner 類實現。

@Component  public class ListennerTest implements CommandLineRunner {        @Autowired      private ThreadTest t;        @Override      public void run(String... args) {          for (int i = 1; i <= 10; i++) {              t.ceshi();          }      }  }
@Component  @Slf4j  public class ThreadTest {        @Async("asyncTaskExecutor")      public void ceshi() {          log.info("ceshi");      }  }    

3、定義一個 http 接口

還可以通過接口的形式來異步調用多線程:

@RestController  @RequestMapping("thread")  public class ListennerTest2 {        @Autowired      private ThreadTest t;        @GetMapping("ceshi2")      public void run() {          for (int i = 1; i < 10; i++) {              t.ceshi2();          }      }  }
@Component  @Slf4j  public class ThreadTest {        @Async("asyncTaskExecutor")      public void ceshi2() {          for (int i = 0; i <= 3; i++) {              log.info("ceshi2");          }      }  }    

4、測試類

@RunWith(SpringRunner.class)  @SpringBootTest  public class ThreadRunTest {        @Autowired      private ThreadTest t;        @Test      public void thread1() {          for (int i = 1; i <= 10; i++) {              t.ceshi4();          }      }  }
@Component  @Slf4j  public class ThreadTest {      @Async("asyncTaskExecutor")      public void ceshi4() {          log.info("ceshi4");      }  }

四、總結

以上主要介紹了 ThreadPoolTaskExecutor 線程池的配置、使用、相關註解的意義及作用,也簡單介紹了使用 @Async 來異步調用線程,最後又列舉了多線程的使用場景,並配上了代碼示例。希望大家喜歡。