天天看點

Spring Boot 2.x基礎教程:如何隔離@Async異步任務的線程池

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

https://blog.didispace.com/spring-boot-learning-2-7-7/#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E9%9A%94%E7%A6%BB%EF%BC%8C%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E9%9A%94%E7%A6%BB 什麼是線程池的隔離,為什麼要隔離

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

@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

建立的異步任務都是共用的一個線程池,是以當有一些異步任務碰到性能問題的時候,是會直接影響其他異步任務的。

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

https://blog.didispace.com/spring-boot-learning-2-7-7/#%E4%B8%8D%E5%90%8C%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E9%85%8D%E7%BD%AE%E4%B8%8D%E5%90%8C%E7%BA%BF%E7%A8%8B%E6%B1%A0 不同異步任務配置不同線程池

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

第一步:初始化多個線程池,比如下面這樣:

@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

注解中定義的

taskExecutor1

taskExecutor2

就是線程池的名字。由于在第一步中,我們沒有具體寫兩個線程池Bean的名稱,是以預設會使用方法名,也就是

taskExecutor1

taskExecutor2

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

@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教程可以點選直達!

,歡迎收藏與轉發支援!

https://blog.didispace.com/spring-boot-learning-2-7-7/#%E4%BB%A3%E7%A0%81%E7%A4%BA%E4%BE%8B 代碼示例

本文的完整工程可以檢視下面倉庫中

2.x

目錄下的

chapter7-7

工程:

如果您覺得本文不錯,歡迎

Star

支援,您的關注是我堅持的動力!