獲取雙非同步返回值時,如何保證主執行緒不阻塞?
來源:哪吒程式設計
一、前情提要
在上一篇文章中,使用雙非同步後,如何保證資料一致性?,透過Future獲取非同步返回值,輪詢判斷Future狀態,如果執行完畢或已取消,則透過get()獲取返回值,get()是阻塞的方法,因此會阻塞當前執行緒,如果透過new Runnable()執行get()方法,那麼還是需要返回AsyncResult,然後再透過主執行緒去get()獲取非同步執行緒返回結果。
寫法很繁瑣,還會阻塞主執行緒。
下面是FutureTask非同步執行流程圖:
二、JDK8的CompletableFuture
1、ForkJoinPool
Java8中引入了CompletableFuture,它實現了對Future的全面升級,可以透過回撥的方式,獲取非同步執行緒返回值。
CompletableFuture的非同步執行透過ForkJoinPool實現, 它使用守護執行緒去執行任務。
ForkJoinPool在於可以充分利用多核CPU的優勢,把一個任務拆分成多個小任務,把多個小任務放到多個CPU上並行執行,當多個小任務執行完畢後,再將其執行結果合併起來。
Future的非同步執行是透過ThreadPoolExecutor實現的。
2、從ForkJoinPool和ThreadPoolExecutor探索CompletableFuture和Future的區別
ForkJoinPool中的每個執行緒都會有一個佇列,而ThreadPoolExecutor只有一個佇列,並根據queue型別不同,細分出各種執行緒池; ForkJoinPool在使用過程中,會建立大量的子任務,會進行大量的gc,但是ThreadPoolExecutor不需要,因為ThreadPoolExecutor是任務分配平均的; ThreadPoolExecutor中每個非同步執行緒之間是相互獨立的,當執行速度快的執行緒執行完畢後,它就會一直處於空閒的狀態,等待其它執行緒執行完畢; ForkJoinPool中每個非同步執行緒之間並不是絕對獨立的,在ForkJoinPool執行緒池中會維護一個佇列來存放需要執行的任務,當執行緒自身任務執行完畢後,它會從其它執行緒中獲取未執行的任務並幫助它執行,直至所有執行緒執行完畢。
因此,在多執行緒任務分配不均時,ForkJoinPool的執行效率更高。但是,如果任務分配均勻,ThreadPoolExecutor的執行效率更高,因為ForkJoinPool會建立大量子任務,並對其進行大量的GC,比較耗時。
三、透過CompletableFuture最佳化 “透過Future獲取非同步返回值”
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)測試環境
12個邏輯處理器的電腦; Excel中包含10萬條資料; Future的自定義執行緒池,核心執行緒數為24; ForkJoinPool的核心執行緒數為24;
(2)統計四種情況下10萬資料入庫時間
不獲取非同步返回值 透過Future獲取非同步返回值 透過CompletableFuture獲取非同步返回值,預設ForkJoinPool執行緒池的核心執行緒數為本機邏輯處理器數量,測試電腦為12; 透過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秒鐘,這還是相當驚人的,最佳化的價值就在於此。
四、透過CompletableFuture.allOf解決阻塞主執行緒問題
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----會阻塞主執行緒嗎?");
五、CompletableFuture中花俏的語法糖
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;
}
六、順序執行非同步任務
1、thenRun
thenRun()不接受引數,也沒有返回值,與runAsync()配套使用,恰到好處。
// JDK8的CompletableFuture
CompletableFuture.runAsync(() -> readExcelDbJdk8Service.readXlsCacheAsyncMybatis())
.thenRun(() -> logger.info("CompletableFuture----.thenRun()方法測試"));
2、thenAccept
thenAccept()接受引數,沒有返回值。
supplyAsync + thenAccept
非同步執行緒順序執行 supplyAsync的非同步返回值,可以作為thenAccept的引數使用 不會阻塞主執行緒
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,典型的鏈式程式設計。
非同步執行緒內方法順序執行 supplyAsync 的返回值,作為第 1 個thenApply的引數,進行業務處理 第 1 個thenApply的返回值,作為第 2 個thenApply的引數,進行業務處理 最後,透過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());
七、CompletableFuture合併任務
thenCombine,多個非同步任務並行處理,有返回值,最後合併結果返回新的CompletableFuture物件; thenAcceptBoth,多個非同步任務並行處理,無返回值; acceptEither,多個非同步任務並行處理,無返回值; applyToEither,,多個非同步任務並行處理,有返回值;
CompletableFuture合併任務的程式碼例項,這裡就不多贅述了,一些語法糖而已,大家切記陷入低水平勤奮的怪圈。
八、CompletableFuture VS Future總結
本文中以下幾個方面對比了CompletableFuture和Future的差異:
ForkJoinPool和ThreadPoolExecutor的實現原理,探索了CompletableFuture和Future的差異; 透過程式碼例項的形式簡單介紹了CompletableFuture中花俏的語法糖; 透過CompletableFuture最佳化了 “透過Future獲取非同步返回值”; 透過CompletableFuture.allOf解決阻塞主執行緒問題。
Future提供了非同步執行的能力,但Future.get()會透過輪詢的方式獲取非同步返回值,get()方法還會阻塞主執行緒。
輪詢的方式非常消耗CPU資源,阻塞的方式顯然與我們的非同步初衷背道而馳。
JDK8提供的CompletableFuture實現了Future介面,新增了很多Future不具備的功能,比如鏈式程式設計、異常處理回撥函式、獲取非同步結果不阻塞不輪詢、合併非同步任務等。
獲取非同步執行緒結果後,我們可以透過新增事務的方式,實現Excel入庫操作的資料一致性。
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024420/viewspace-3005054/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 非同步/同步,阻塞/非阻塞,單執行緒/多執行緒概念梳理非同步執行緒
- 程式執行緒、同步非同步、阻塞非阻塞、併發並行執行緒非同步並行
- 併發-0-同步/非同步/阻塞/非阻塞/程式/執行緒非同步執行緒
- 聊聊執行緒與程式 & 阻塞與非阻塞 & 同步與非同步執行緒非同步
- 對執行緒、協程和同步非同步、阻塞非阻塞的理解執行緒非同步
- 保證執行緒在主執行緒執行執行緒
- 程式與執行緒、同步與非同步、阻塞與非阻塞、併發與並行執行緒非同步並行
- 伺服器模型——從單執行緒阻塞到多執行緒非阻塞(下)伺服器模型執行緒
- 伺服器模型——從單執行緒阻塞到多執行緒非阻塞(中)伺服器模型執行緒
- suging閒談-netty 的非同步非阻塞IO執行緒與業務執行緒分離Netty非同步執行緒
- Python建立多執行緒任務並獲取每個執行緒返回值Python執行緒
- Python中獲取執行緒返回值的常用方法!Python執行緒
- 獲取Java執行緒返回值的幾種方式Java執行緒
- 單執行緒-非阻塞-長連結執行緒
- Java中如何保證執行緒順序執行Java執行緒
- 非同步阻塞,Manager模組,執行緒非同步執行緒
- Node.js 執行 shell 命令 主程式獲取返回值Node.js
- iOS 在主執行緒操作UI不能保證安全iOS執行緒UI
- 同步非同步,阻塞非阻塞非同步
- 非同步、同步、阻塞、非阻塞非同步
- Java下如何保證多執行緒安全Java執行緒
- 【java】【多執行緒】獲取和設定執行緒名字、獲取執行緒物件(3)Java執行緒物件
- Java如何獲取當前執行緒Java執行緒
- Java併發程式設計(二)如何保證執行緒同時/交替執行Java程式設計執行緒
- 同步、非同步,阻塞、非阻塞理解非同步
- 同步、非同步、阻塞與非阻塞非同步
- 同步非同步 與 阻塞非阻塞非同步
- 理解阻塞、非阻塞、同步、非同步非同步
- ♻️同步和非同步;並行和併發;阻塞和非阻塞非同步並行
- 詳細介紹C++多執行緒獲取返回值的方法C++執行緒
- 同步、非同步、阻塞、非阻塞的區別非同步
- 在 Flink 運算元中使用多執行緒如何保證不丟資料?執行緒
- 服務重啟了,如何保證執行緒池中的資料不丟失?執行緒
- 如何解讀 Java IO、NIO 中的同步阻塞與同步非阻塞?Java
- 在非主執行緒中建立視窗執行緒
- 保證執行緒安全的技術執行緒
- 【OS】同步非同步/阻塞非阻塞、併發並行序列的區分非同步並行
- IO - 同步 非同步 阻塞 非阻塞的區別非同步