CompletableFuture get方法一直阻塞或丟擲TimeoutException
問題描述
最近剛剛上線的服務突然丟擲大量的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的執行緒做執行緒池隔離,讓耗時操作不影響主執行緒的執行
總結
future.get(5, TimeUnit.SECONDS);
原始碼
ofollow,noindex">https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-completable-future 工程
為監控而生的多級快取框架 layering-cache 這是我開源的一個多級快取框架的實現,如果有興趣可以看一下。