獲取雙非同步返回值時,如何保證主執行緒不阻塞?

ITPUB社群發表於2024-01-25

來源:哪吒程式設計

unsetunset一、前情提要unsetunset

在上一篇文章中,使用雙非同步後,如何保證資料一致性?,透過Future獲取非同步返回值,輪詢判斷Future狀態,如果執行完畢或已取消,則透過get()獲取返回值,get()是阻塞的方法,因此會阻塞當前執行緒,如果透過new Runnable()執行get()方法,那麼還是需要返回AsyncResult,然後再透過主執行緒去get()獲取非同步執行緒返回結果。

寫法很繁瑣,還會阻塞主執行緒。

下面是FutureTask非同步執行流程圖:

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

unsetunset二、JDK8的CompletableFutureunsetunset

1、ForkJoinPool

Java8中引入了CompletableFuture,它實現了對Future的全面升級,可以透過回撥的方式,獲取非同步執行緒返回值。

CompletableFuture的非同步執行透過ForkJoinPool實現, 它使用守護執行緒去執行任務。

ForkJoinPool在於可以充分利用多核CPU的優勢,把一個任務拆分成多個小任務,把多個小任務放到多個CPU上並行執行,當多個小任務執行完畢後,再將其執行結果合併起來。

Future的非同步執行是透過ThreadPoolExecutor實現的。

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

2、從ForkJoinPool和ThreadPoolExecutor探索CompletableFuture和Future的區別

  1. ForkJoinPool中的每個執行緒都會有一個佇列,而ThreadPoolExecutor只有一個佇列,並根據queue型別不同,細分出各種執行緒池;
  2. ForkJoinPool在使用過程中,會建立大量的子任務,會進行大量的gc,但是ThreadPoolExecutor不需要,因為ThreadPoolExecutor是任務分配平均的;
  3. ThreadPoolExecutor中每個非同步執行緒之間是相互獨立的,當執行速度快的執行緒執行完畢後,它就會一直處於空閒的狀態,等待其它執行緒執行完畢;
  4. ForkJoinPool中每個非同步執行緒之間並不是絕對獨立的,在ForkJoinPool執行緒池中會維護一個佇列來存放需要執行的任務,當執行緒自身任務執行完畢後,它會從其它執行緒中獲取未執行的任務並幫助它執行,直至所有執行緒執行完畢。

因此,在多執行緒任務分配不均時,ForkJoinPool的執行效率更高。但是,如果任務分配均勻,ThreadPoolExecutor的執行效率更高,因為ForkJoinPool會建立大量子任務,並對其進行大量的GC,比較耗時。

unsetunset三、透過CompletableFuture最佳化 “透過Future獲取非同步返回值”unsetunset

1、透過Future獲取非同步返回值關鍵程式碼

(1)將非同步方法的返回值改為Future<Integer>,將返回值放到new AsyncResult<>();中;

@Async("async-executor")
public void readXls(String filePath, String filename) {
    try {
     // 此程式碼為簡化關鍵性程式碼
        List<Future<Integer>> futureList = new ArrayList<>();
        for (int time = 0; time < times; time++) {
            Future<Integer> sumFuture = readExcelDataAsyncFutureService.readXlsCacheAsync();
            futureList.add(sumFuture);
        }
    }catch (Exception e){
        logger.error("readXlsCacheAsync---插入資料異常:",e);
    }
}
@Async("async-executor")
public Future<Integer> readXlsCacheAsync() {
    try {
        // 此程式碼為簡化關鍵性程式碼
        return new AsyncResult<>(sum);
    }catch (Exception e){
        return new AsyncResult<>(0);
    }
}

(2)透過Future<Integer>.get()獲取返回值:

public static boolean getFutureResult(List<Future<Integer>> futureList, int excelRow) {
    int[] futureSumArr = new int[futureList.size()];
    for (int i = 0;i<futureList.size();i++) {
        try {
            Future<Integer> future = futureList.get(i);
            while (true) {
                if (future.isDone() && !future.isCancelled()) {
                    Integer futureSum = future.get();
                    logger.info("獲取Future返回值成功"+"----Future:" + future
                            + ",Result:" + futureSum);
                    futureSumArr[i] += futureSum;
                    break;
                } else {
                    logger.info("Future正在執行---獲取Future返回值中---等待3秒");
                    Thread.sleep(3000);
                }
            }
        } catch (Exception e) {
            logger.error("獲取Future返回值異常: ", e);
        }
    }
    
    boolean insertFlag = getInsertSum(futureSumArr, excelRow);
    logger.info("獲取所有非同步執行緒Future的返回值成功,Excel插入結果="+insertFlag);
    return insertFlag;
}

2、透過CompletableFuture獲取非同步返回值關鍵程式碼

(1)將非同步方法的返回值改為 int

@Async("async-executor")
public void readXls(String filePath, String filename) {
 List<CompletableFuture<Integer>> completableFutureList = new ArrayList<>();
    for (int time = 0; time < times; time++) {
     // 此程式碼為簡化關鍵性程式碼
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(new Supplier<Integer>() {
         @Override
         public Integer get() {
             return readExcelDbJdk8Service.readXlsCacheAsyncMybatis();
         }
     }).thenApply((result) -> {// 回撥方法
         return thenApplyTest2(result);// supplyAsync返回值 * 1
     }).thenApply((result) -> {
         return thenApplyTest5(result);// thenApply返回值 * 1
     }).exceptionally((e) -> { // 如果執行異常:
         logger.error("CompletableFuture.supplyAsync----異常:", e);
         return null;
     });
 
     completableFutureList.add(completableFuture);
    }
}
@Async("async-executor")
public int readXlsCacheAsync() {
    try {
        // 此程式碼為簡化關鍵性程式碼
        return sum;
    }catch (Exception e){
        return -1;
    }
}

(2)透過completableFuture.get()獲取返回值

public static boolean getCompletableFutureResult(List<CompletableFuture<Integer>> list, int excelRow){
    logger.info("透過completableFuture.get()獲取每個非同步執行緒的插入結果----開始");

    int sum = 0;
    for (int i = 0; i < list.size(); i++) {
        Integer result = list.get(i).get();
        sum += result;
    }

    boolean insertFlag = excelRow == sum;
    logger.info("全部執行完畢,excelRow={},入庫={}, 資料是否一致={}",excelRow,sum,insertFlag);
    return insertFlag;
}

3、效率對比

(1)測試環境

  1. 12個邏輯處理器的電腦;
  2. Excel中包含10萬條資料;
  3. Future的自定義執行緒池,核心執行緒數為24;
  4. ForkJoinPool的核心執行緒數為24;

(2)統計四種情況下10萬資料入庫時間

  1. 不獲取非同步返回值
  2. 透過Future獲取非同步返回值
  3. 透過CompletableFuture獲取非同步返回值,預設ForkJoinPool執行緒池的核心執行緒數為本機邏輯處理器數量,測試電腦為12;
  4. 透過CompletableFuture獲取非同步返回值,修改ForkJoinPool執行緒池的核心執行緒數為24。

備註:因為CompletableFuture不阻塞主執行緒,主執行緒執行時間只有2秒,表格中統計的是非同步執行緒全部執行完成的時間。

(3)設定核心執行緒數

將核心執行緒數CorePoolSize設定成CPU的處理器數量,是不是效率最高的?

// 獲取CPU的處理器數量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;// 測試電腦是24

因為在介面被呼叫後,開啟非同步執行緒,執行入庫任務,因為測試機最多同時開啟24執行緒處理任務,故將10萬條資料拆分成等量的24份,也就是10萬/24 = 4166,那麼我設定成4200,是不是效率最佳呢?

測試的過程中發現,好像真的是這樣的。

自定義ForkJoinPool執行緒池
@Autowired
@Qualifier("asyncTaskExecutor")
private Executor asyncTaskExecutor;

@Override
public void readXls(String filePath, String filename) {
  List<CompletableFuture<Integer>> completableFutureList = new ArrayList<>();
    for (int time = 0; time < times; time++) {
  CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(new Supplier<Integer>() {
         @Override
         public Integer get() {
             try {
                 return readExcelDbJdk8Service.readXlsCacheAsync(sheet, row, start, finalEnd, insertBuilder);
             } catch (Exception e) {
                 logger.error("CompletableFuture----readXlsCacheAsync---異常:", e);
                 return -1;
             }
         };
     },asyncTaskExecutor);
 
     completableFutureList.add(completableFuture);
 }

 // 不會阻塞主執行緒
    CompletableFuture.allOf(completableFutureList.toArray(new CompletableFuture[completableFutureList.size()])).whenComplete((r,e) -> {
        try {
            int insertSum = getCompletableFutureResult(completableFutureList, excelRow);
        } catch (Exception ex) {
            return;
        }
    });
}
自定義執行緒池
/**
 * 自定義非同步執行緒池
 */

@Bean("asyncTaskExecutor")
public AsyncTaskExecutor asyncTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    //設定執行緒名稱
    executor.setThreadNamePrefix("asyncTask-Executor");
    //設定最大執行緒數
    executor.setMaxPoolSize(200);
    //設定核心執行緒數
    executor.setCorePoolSize(24);
    //設定執行緒空閒時間,預設60
    executor.setKeepAliveSeconds(200);
    //設定佇列容量
    executor.setQueueCapacity(50);
    /**
     * 當執行緒池的任務快取佇列已滿並且執行緒池中的執行緒數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略
     * 通常有以下四種策略:
     * ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。
     * ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
     * ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
     * ThreadPoolExecutor.CallerRunsPolicy:重試新增當前的任務,自動重複呼叫 execute() 方法,直到成功
     */

    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
}

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

(4)統計分析

效率對比:

③透過CompletableFuture獲取非同步返回值(12執行緒) <  ②透過Future獲取非同步返回值 <  ④透過CompletableFuture獲取非同步返回值(24執行緒) <  ①不獲取非同步返回值

不獲取非同步返回值時效能最優,這不廢話嘛~

核心執行緒數相同的情況下,CompletableFuture的入庫效率要優於Future的入庫效率,10萬條資料大概要快4秒鐘,這還是相當驚人的,最佳化的價值就在於此。

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

unsetunset四、透過CompletableFuture.allOf解決阻塞主執行緒問題unsetunset

1、語法

CompletableFuture.allOf(CompletableFuture的可變陣列).whenComplete((r,e) -> {})

2、程式碼例項

getCompletableFutureResult方法在 “3.2.2 透過completableFuture.get()獲取返回值”。

// 不會阻塞主執行緒
CompletableFuture.allOf(completableFutureList.toArray(new   CompletableFuture[completableFutureList.size()])).whenComplete((r,e) -> {
    logger.info("全部執行完畢,解決主執行緒阻塞問題~");
    try {
        int insertSum = getCompletableFutureResult(completableFutureList, excelRow);
    } catch (Exception ex) {
        logger.error("全部執行完畢,解決主執行緒阻塞問題,異常:", ex);
        return;
    }
});

// 會阻塞主執行緒
//getCompletableFutureResult(completableFutureList, excelRow);

logger.info("CompletableFuture----會阻塞主執行緒嗎?");

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

unsetunset五、CompletableFuture中花俏的語法糖unsetunset

1、runAsync

runAsync 方法不支援返回值。

可以透過runAsync執行沒有返回值的非同步方法。

不會阻塞主執行緒。

// 分批非同步讀取Excel內容併入庫
int finalEnd = end;
CompletableFuture.runAsync(() -> readExcelDbJdk8Service.readXlsCacheAsyncMybatis();

2、supplyAsync

supplyAsync也可以非同步處理任務,傳入的物件實現了Supplier介面。將Supplier作為引數並返回CompletableFuture

會阻塞主執行緒。

supplyAsync()方法關鍵程式碼:

int finalEnd = end;
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(new Supplier<Integer>() {
    @Override
    public Integer get() {
        return readExcelDbJdk8Service.readXlsCacheAsyncMybatis();
    }
});
@Override
public int readXlsCacheAsyncMybatis() {
    // 不為人知的操作
    // 返回非同步方法執行結果即可
 return 100;
}

unsetunset六、順序執行非同步任務unsetunset

1、thenRun

thenRun()不接受引數,也沒有返回值,與runAsync()配套使用,恰到好處。

// JDK8的CompletableFuture
CompletableFuture.runAsync(() -> readExcelDbJdk8Service.readXlsCacheAsyncMybatis())
.thenRun(() -> logger.info("CompletableFuture----.thenRun()方法測試"));

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

2、thenAccept

thenAccept()接受引數,沒有返回值。

supplyAsync + thenAccept

  1. 非同步執行緒順序執行
  2. supplyAsync的非同步返回值,可以作為thenAccept的引數使用
  3. 不會阻塞主執行緒
CompletableFuture.supplyAsync(new Supplier<Integer>() {
    @Override
    public Integer get() {
        return readExcelDbJdk8Service.readXlsCacheAsyncMybatis();
    }
}).thenAccept(x -> logger.info(".thenAccept()方法測試:" + x));

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

但是,此時無法透過completableFuture.get()獲取supplyAsync的返回值了。

3、thenApply

thenApply在thenAccept的基礎上,可以再次透過completableFuture.get()獲取返回值。

supplyAsync + thenApply,典型的鏈式程式設計。

  1. 非同步執行緒內方法順序執行
  2. supplyAsync 的返回值,作為第 1 個thenApply的引數,進行業務處理
  3. 第 1 個thenApply的返回值,作為第 2 個thenApply的引數,進行業務處理
  4. 最後,透過future.get()方法獲取最終的返回值
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(new Supplier<Integer>() {
 @Override
    public Integer get() {
        return readExcelDbJdk8Service.readXlsCacheAsyncMybatis();
    }
}).thenApply((result) -> {
    return thenApplyTest2(result);// supplyAsync返回值 * 2
}).thenApply((result) -> {
    return thenApplyTest5(result);// thenApply返回值 * 5
});

logger.info("readXlsCacheAsyncMybatis插入資料 * 2 * 5 = " + completableFuture.get());

獲取雙非同步返回值時,如何保證主執行緒不阻塞?

unsetunset七、CompletableFuture合併任務unsetunset

  1. thenCombine,多個非同步任務並行處理,有返回值,最後合併結果返回新的CompletableFuture物件;
  2. thenAcceptBoth,多個非同步任務並行處理,無返回值;
  3. acceptEither,多個非同步任務並行處理,無返回值;
  4. applyToEither,,多個非同步任務並行處理,有返回值;

CompletableFuture合併任務的程式碼例項,這裡就不多贅述了,一些語法糖而已,大家切記陷入低水平勤奮的怪圈。

unsetunset八、CompletableFuture VS Future總結unsetunset

本文中以下幾個方面對比了CompletableFuture和Future的差異:

  1. ForkJoinPool和ThreadPoolExecutor的實現原理,探索了CompletableFuture和Future的差異;
  2. 透過程式碼例項的形式簡單介紹了CompletableFuture中花俏的語法糖;
  3. 透過CompletableFuture最佳化了 “透過Future獲取非同步返回值”;
  4. 透過CompletableFuture.allOf解決阻塞主執行緒問題。

Future提供了非同步執行的能力,但Future.get()會透過輪詢的方式獲取非同步返回值,get()方法還會阻塞主執行緒。

輪詢的方式非常消耗CPU資源,阻塞的方式顯然與我們的非同步初衷背道而馳。

JDK8提供的CompletableFuture實現了Future介面,新增了很多Future不具備的功能,比如鏈式程式設計、異常處理回撥函式、獲取非同步結果不阻塞不輪詢、合併非同步任務等。

獲取非同步執行緒結果後,我們可以透過新增事務的方式,實現Excel入庫操作的資料一致性。

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024420/viewspace-3005054/,如需轉載,請註明出處,否則將追究法律責任。

相關文章