1. 程式人生 > 程式設計 >Spring使用ThreadPoolTaskExecutor自定義執行緒池及實現非同步呼叫

Spring使用ThreadPoolTaskExecutor自定義執行緒池及實現非同步呼叫

多執行緒一直是工作或面試過程中的高頻知識點,今天給大家分享一下使用 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 來非同步呼叫執行緒,最後又列舉了多執行緒的使用場景,並配上了程式碼示例。希望大家喜歡。