Spring Boot中有多個@Async非同步任務時,記得做好執行緒池的隔離!

程式猿DD 發表於 2021-09-18
Spring

通過上一篇:配置@Async非同步任務的執行緒池的介紹,你應該已經瞭解到非同步任務的執行背後有一個執行緒池來管理執行任務。為了控制非同步任務的併發不影響到應用的正常運作,我們必須要對執行緒池做好相應的配置,防止資源的過渡使用。除了預設執行緒池的配置之外,還有一類場景,也是很常見的,那就是多工情況下的執行緒池隔離。

什麼是執行緒池的隔離,為什麼要隔離

可能有的小夥伴還不太瞭解什麼是執行緒池的隔離,為什麼要隔離?。所以,我們先來看看下面的場景案例:

@RestController
public class HelloController {

    @Autowired
    private AsyncTasks asyncTasks;
        
    @GetMapping("/api-1")
    public String taskOne() {
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");
        
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }
    
    @GetMapping("/api-2")
    public String taskTwo() {
        CompletableFuture<String> task1 = asyncTasks.doTaskTwo("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskTwo("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskTwo("3");
        
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }
    
}

上面的程式碼中,有兩個API介面,這兩個介面的具體執行邏輯中都會把執行過程拆分為三個非同步任務來實現。

好了,思考一分鐘,想一下。如果這樣實現,會有什麼問題嗎?


上面這段程式碼,在API請求併發不高,同時如果每個任務的處理速度也夠快的時候,是沒有問題的。但如果併發上來或其中某幾個處理過程扯後腿了的時候。這兩個提供不相干服務的介面可能會互相影響。比如:假設當前執行緒池配置的最大執行緒數有2個,這個時候/api-1介面中task1和task2處理速度很慢,阻塞了;那麼此時,當使用者呼叫api-2介面的時候,這個服務也會阻塞!

造成這種現場的原因是:預設情況下,所有用@Async建立的非同步任務都是共用的一個執行緒池,所以當有一些非同步任務碰到效能問題的時候,是會直接影響其他非同步任務的。

為了解決這個問題,我們就需要對非同步任務做一定的執行緒池隔離,讓不同的非同步任務互不影響。

不同非同步任務配置不同執行緒池

下面,我們就來實際操作一下!

第一步:初始化多個執行緒池,比如下面這樣:

@EnableAsync
@Configuration
public class TaskPoolConfig {

    @Bean
    public Executor taskExecutor1() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-1-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    @Bean
    public Executor taskExecutor2() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-2-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

注意:這裡特地用executor.setThreadNamePrefix設定了執行緒名的字首,這樣可以方便觀察後面具體執行的順序。

第二步:建立非同步任務,並指定要使用的執行緒池名稱

@Slf4j
@Component
public class AsyncTasks {

    public static Random random = new Random();

    @Async("taskExecutor1")
    public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
        log.info("開始任務:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任務:{},耗時:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任務完成");
    }

    @Async("taskExecutor2")
    public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {
        log.info("開始任務:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任務:{},耗時:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任務完成");
    }

}

這裡@Async註解中定義的taskExecutor1taskExecutor2就是執行緒池的名字。由於在第一步中,我們沒有具體寫兩個執行緒池Bean的名稱,所以預設會使用方法名,也就是taskExecutor1taskExecutor2

第三步:寫個單元測試來驗證下,比如下面這樣:

@Slf4j
@SpringBootTest
public class Chapter77ApplicationTests {

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception {
        long start = System.currentTimeMillis();

        // 執行緒池1
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");

        // 執行緒池2
        CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");
        CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");
        CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6");

        // 一起執行
        CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();

        long end = System.currentTimeMillis();

        log.info("任務全部完成,總耗時:" + (end - start) + "毫秒");
    }

}

在上面的單元測試中,一共啟動了6個非同步任務,前三個用的是執行緒池1,後三個用的是執行緒池2。

先不執行,根據設定的核心執行緒2和最大執行緒數2,來分析一下,大概會是怎麼樣的執行情況?

  1. 執行緒池1的三個任務,task1和task2會先獲得執行執行緒,然後task3因為沒有可分配執行緒進入緩衝佇列
  2. 執行緒池2的三個任務,task4和task5會先獲得執行執行緒,然後task6因為沒有可分配執行緒進入緩衝佇列
  3. 任務task3會在task1或task2完成之後,開始執行
  4. 任務task6會在task4或task5完成之後,開始執行

分析好之後,執行下單元測試,看看是否是這樣的:

2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-1] com.didispace.chapter77.AsyncTasks       : 開始任務:1
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-2-2] com.didispace.chapter77.AsyncTasks       : 開始任務:5
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 開始任務:4
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 開始任務:2
2021-09-15 23:45:15.905  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 完成任務:4,耗時:4532 毫秒
2021-09-15 23:45:15.905  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 開始任務:6
2021-09-15 23:45:18.263  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 完成任務:2,耗時:6890 毫秒
2021-09-15 23:45:18.263  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 開始任務:3
2021-09-15 23:45:18.896  INFO 61670 --- [   executor-2-2] com.didispace.chapter77.AsyncTasks       : 完成任務:5,耗時:7523 毫秒
2021-09-15 23:45:19.842  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 完成任務:3,耗時:1579 毫秒
2021-09-15 23:45:20.551  INFO 61670 --- [   executor-1-1] com.didispace.chapter77.AsyncTasks       : 完成任務:1,耗時:9178 毫秒
2021-09-15 23:45:24.117  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 完成任務:6,耗時:8212 毫秒
2021-09-15 23:45:24.117  INFO 61670 --- [           main] c.d.chapter77.Chapter77ApplicationTests  : 任務全部完成,總耗時:12762毫秒

好了,今天的學習就到這裡!如果您學習過程中如遇困難?可以加入我們超高質量的Spring技術交流群,參與交流與討論,更好的學習與進步!更多Spring Boot教程可以點選直達!,歡迎收藏與轉發支援!

程式碼示例

本文的完整工程可以檢視下面倉庫中2.x目錄下的chapter7-7工程:

如果您覺得本文不錯,歡迎Star支援,您的關注是我堅持的動力!

歡迎關注我的公眾號:程式猿DD,分享外面看不到的乾貨與思考!