在日常開發中如果使用 CompletableFuture 的時候,如果出現異常,會怎麼樣呢,會只停止這個執行緒還是全部停止?會主動丟擲異常嗎,今天我透過一下案例來以結果來分析。
開發環境
# java 環境 openjdk 23.0.1 2024-10-15 OpenJDK Runtime Environment (build 23.0.1+11-39) OpenJDK 64-Bit Server VM (build 23.0.1+11-39, mixed mode, sharing)
兩個 CompletableFuture 一個拋異常 一個不拋異常,執行結果是什麼呢
/** * 不拋異常 */ public CompletableFuture<Void> normalCompletableFuture() { return CompletableFuture.runAsync(() -> { // 這裏讓執行緒休眠10秒 try { Thread.sleep(3000); } catch (InterruptedException e) { log.info("睡眠過程中出現異常 -> {}", e); } finally { log.info("普通執行緒執行完畢"); } }); } /** * 拋異常的執行緒 */ public CompletableFuture<Void> exceptionCompletableFuture() { return CompletableFuture.runAsync(() -> { throw new RuntimeException("runAsync -> 發生了一個大的異常"); }); }
然後可以使用 allOf 來組合兩個執行緒
public void testRunRunAsync() { CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(this.exceptionCompletableFuture(), this.normalCompletableFuture()); voidCompletableFuture.join(); }
執行以後得到以下的執行結果
09:22:56.319 [ForkJoinPool.commonPool-worker-1] INFO org.flow.basic.threadpool.CompletableFutureRuntimeDemo -- 普通執行緒執行完畢 java.lang.RuntimeException: runAsync -> 發生了一個大的異常 at org.flow.basic.threadpool.CompletableFutureRuntimeDemo.lambda$exceptionCompletableFuture$1(CompletableFutureRuntimeDemo.java:38) at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1848) at java.base/java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1840) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:507) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1458) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:2034) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:189)
我們可以看到雖然丟擲異常了,但是並不會因為異常而讓全部的執行的任務都停止,他會等待所有的任務都執行完畢,然後丟擲異常。那如果不實用 join 使用其他的 api 是否會丟擲異常呢? 把執行的程式碼略作改動我們再來執行看看效果
public void testRunRunAsync() { CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(this.exceptionCompletableFuture(), this.normalCompletableFuture()); while (true) { if (voidCompletableFuture.isDone()) { log.info("執行緒執行完畢"); break; } } }
執行結果
09:36:27.601 [ForkJoinPool.commonPool-worker-1] INFO org.flow.basic.threadpool.CompletableFutureRuntimeDemo -- 普通執行緒執行完畢 09:36:27.604 [main] INFO org.flow.basic.threadpool.CompletableFutureRuntimeDemo -- 執行緒執行完畢
這個可以看出我們並沒有使用阻塞的方式,而是使用了 while 的死迴圈,這個會對 cpu 帶來壓力,不過我麼可以看到他並不會報錯並且兩個任務全部執行完畢。
兩個 CompletableFuture 一個拋異常 一個不拋異常,如果執行呢?
public void testRunRunAsync() { CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(this.exceptionCompletableFuture(), this.normalCompletableFuture()); voidCompletableFuture.exceptionally(e -> { log.error("發生異常,主動處理 -> {}", e.getMessage()); return null; }).join(); log.info("正常執行完畢"); }
列印結果
09:51:36.359 [ForkJoinPool.commonPool-worker-1] INFO org.flow.basic.threadpool.CompletableFutureRuntimeDemo -- 普通執行緒執行完畢 09:51:36.362 [ForkJoinPool.commonPool-worker-1] ERROR org.flow.basic.threadpool.CompletableFutureRuntimeDemo -- 發生異常,主動處理 -> java.lang.RuntimeException: runAsync -> 發生了一個大的異常 09:51:36.363 [main] INFO org.flow.basic.threadpool.CompletableFutureRuntimeDemo -- 正常執行完畢
這個我們可以看出如果主動處理了異常,但是沒有使用 join 或者 isDone 等方式來判斷,那麼就會導致程式阻塞,並且不會丟擲異常。
結論
CompletableFuture 的 allOf 的時候如果出現異常,不會主動丟擲異常,但是會等待所有的任務都執行完畢,然後丟擲異常。如果不用 join 等待,也可以透過 isDone 來判斷是否執行完畢,這個並不會主動丟擲異常,這個要自己來判斷異常。但是會對 CPU 帶來負載,這個時候可以根據實際情況來做衡量取捨。但是如果使用了 exceptionally 來處理異常,那麼就會被異常處理攔截會根據自己的處理來做實際的異常行為