CompletableFuture get方法一直阻塞或拋出TimeoutException

問題描述

最近剛剛上線的服務忽然拋出大量的TimeoutException,查詢後發現是使用了CompletableFuture,而且在執行future.get(5, TimeUnit.SECONDS);時拋出了TimeoutException異常,致使接口響應很慢進而影響了其餘系統的調用。java

問題分析

首先咱們知道CompletableFuture的get()方法值會阻塞主線程,直到子線程執行任務完成返回結果纔會取消阻塞。若是子線程一直不返回接口那麼主線程就會一直阻塞,因此咱們通常不建議直接使用CompletableFuture的get()方法,而是使用future.get(5, TimeUnit.SECONDS);方法指定超時時間。git

可是當咱們的線程池拒絕策略使用的是DiscardPolicy或者DiscardOldestPolicy,而且線程池飽和了的時候,咱們將會直接丟棄任務,不會拋出任何異常。這個時候再來調用get方法是主線程就會一直等待子線程返回結果,直到超時拋出TimeoutException。github

咱們來看下面一段代碼:spring

@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框架

spring-boot-student-completable-future 工程異步

爲監控而生的多級緩存框架 layering-cache這是我開源的一個多級緩存框架的實現,若是有興趣能夠看一下。分佈式

相關文章
相關標籤/搜索