問題描述
最近剛剛上線的服務突然抛出大量的TimeoutException,查詢後發現是使用了CompletableFuture,并且在執行
future.get(5, TimeUnit.SECONDS);
時抛出了TimeoutException異常,導緻接口響應很慢進而影響了其他系統的調用。
問題分析
首先我們知道CompletableFuture的get()方法值會阻塞主線程,直到子線程執行任務完成傳回結果才會取消阻塞。如果子線程一直不傳回接口那麼主線程就會一直阻塞,是以我們一般不建議直接使用CompletableFuture的get()方法,而是使用
future.get(5, TimeUnit.SECONDS);
方法指定逾時時間。
但是當我們的線程池拒絕政策使用的是DiscardPolicy或者DiscardOldestPolicy,并且線程池飽和了的時候,我們将會直接丢棄任務,不會抛出任何異常。這個時候再來調用get方法是主線程就會一直等待子線程傳回結果,直到逾時抛出TimeoutException。
我們來看下面一段代碼:
@RunWith(SpringRunner.class)
@SpringBootTest
public class CompletableFutureTest {
Logger logger = LoggerFactory.getLogger(CompletableFutureTest.class);
ThreadPoolTaskExecutor taskExecutor = null;
@Before
public void before() {
taskExecutor = new ThreadPoolTaskExecutor();
// 核心線程數
taskExecutor.setCorePoolSize(1);
// 最大線程數
taskExecutor.setMaxPoolSize(1);
// 隊列最大長度
taskExecutor.setQueueCapacity(2);
// 線程池維護線程所允許的空閑時間(機關秒)
taskExecutor.setKeepAliveSeconds(60);
/*
* 線程池對拒絕任務(無限程可用)的處理政策
* ThreadPoolExecutor.AbortPolicy:丢棄任務并抛出RejectedExecutionException異常。
* ThreadPoolExecutor.DiscardPolicy:也是丢棄任務,但是不抛出異常。
* ThreadPoolExecutor.DiscardOldestPolicy:丢棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
* ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務,如果執行器已關閉,則丢棄.
*/
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
taskExecutor.initialize();
}
@Test
public void testGet() throws Exception {
for (int i = 1; i < 100; i++) {
new Thread(() -> {
// 第一步非常耗時,會沾滿線程池
taskExecutor.execute(() -> {
sleep(5000);
});
// 第二步不耗時的操作,但是get的時候會報TimeoutException
CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> 1, taskExecutor);
CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> 2, taskExecutor);
try {
System.out.println(Thread.currentThread().getName() + "::value1" + future1.get(1, TimeUnit.SECONDS));
System.out.println(Thread.currentThread().getName() + "::value2" + future2.get(1, TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
sleep(30000);
}
/**
* @param millis 毫秒
* @Title: sleep
* @Description: 線程等待時間
* @author yuhao.wang
*/
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
logger.info("擷取分布式鎖休眠被中斷:", e);
}
}
}
我們可以看到第一步的異步線程時一個非常耗時的線程,第二步的兩個CompletableFuture是一個非常快的異步操作。按照道理來說
future1.get(1, TimeUnit.SECONDS)
這一步是不因該報TimeOut的。但是我們發現我們線程池拒絕政策使用的是DiscardPolicy,當線程池滿了會直接丢棄任務,而不會終止主線程。這個時候執行get方法的時候,主線線程一直會等待直到逾時為止。是以接口響應速度一下就慢了下來。
解決方案
- 在使用CompletableFuture時線程池拒絕政策最好使用AbortPolicy。直接中斷主線程,達到快速失敗的效果。
- 耗時的異步線程和CompletableFuture的線程做線程池隔離,讓耗時操作不影響主線程的執行
總結
- 在使用CompletableFuture的時候線程池拒絕政策最好使用AbortPolicy,如果線程池滿了直接抛出異常中斷主線程,達到快速失敗的效果
- 耗時的異步線程和CompletableFuture的線程做線程池隔離,讓耗時操作不影響主線程的執行
- 不建議直接使用CompletableFuture的get()方法,而是使用
方法指定逾時時間
future.get(5, TimeUnit.SECONDS);
源碼
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases