天天看點

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方法的時候,主線線程一直會等待直到逾時為止。是以接口響應速度一下就慢了下來。

解決方案

  1. 在使用CompletableFuture時線程池拒絕政策最好使用AbortPolicy。直接中斷主線程,達到快速失敗的效果。
  2. 耗時的異步線程和CompletableFuture的線程做線程池隔離,讓耗時操作不影響主線程的執行

總結

  1. 在使用CompletableFuture的時候線程池拒絕政策最好使用AbortPolicy,如果線程池滿了直接抛出異常中斷主線程,達到快速失敗的效果
  2. 耗時的異步線程和CompletableFuture的線程做線程池隔離,讓耗時操作不影響主線程的執行
  3. 不建議直接使用CompletableFuture的get()方法,而是使用​

    ​future.get(5, TimeUnit.SECONDS);​

    ​方法指定逾時時間

源碼

​​https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases​​

繼續閱讀