為了控制異步任務的並發不影響到應用的正常運作,我們必須要對線程池做好相應的配置,防止資源的過渡使用。除了默認線程池的配置之外,還有一類場景,也是很常見的,那就是多任務情況下的線程池隔離。
什么是線程池的隔離,為什么要隔離
可能有的小伙伴還不太了解 什么是線程池的隔離,為什么要隔離 ?。所以,我們先來看看下面的場景案例:
@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 注解中定義的 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的三個任務,task1和task2會先獲得執行線程,然后task3因為沒有可分配線程進入緩沖隊列
- 線程池2的三個任務,task4和task5會先獲得執行線程,然后task6因為沒有可分配線程進入緩沖隊列
- 任務task3會在task1或task2完成之后,開始執行
- 任務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毫秒