CompletableFuture 中 thenApply() 與 thenApplyAsync() 之間的差異
一、簡介
在CompletableFuture
框架中, thenApply()
和thenApplyAsync()
是促進非同步程式設計的關鍵方法。
在本教程中,我們將深入研究CompletableFuture
中thenApply()
和thenApplyAsync()
之間的差異。我們將探討它們的功能、用例以及何時選擇其中一種。
2. 理解thenApply()
和thenApplyAsync()
CompletableFuture
提供了thenApply()
和thenApplyAsync()
方法,用於將轉換應用於計算結果。這兩種方法都可以對CompletableFuture
的結果執行連結操作。
2.1. thenApply()
thenApply()
是一種用於在CompletableFuture
完成時將函數應用於其結果的方法。它接受Function
函數式接口,將該函數應用於結果,並傳回一個帶有轉換結果的新CompletableFuture
。
2.2. thenApplyAsync()
thenApplyAsync()
是非同步執行所提供函數的方法。它接受一個Function
功能介面和一個可選的Executor
,並傳回一個新的CompletableFuture
和一個非同步轉換的結果。
3. 執行緒
thenApply()
和thenApplyAsync()
之間的主要差異在於它們的執行行為。
3.1. thenApply()
預設情況下, thenApply()
方法使用完成目前CompletableFuture
相同執行緒執行轉換函數。這意味著轉換函數的執行可以在結果可用後立即發生。如果轉換函數長時間運行或佔用資源,這可能會阻塞執行緒。
但是,如果我們在尚未完成的CompletableFuture
上呼叫thenApply()
,它將在執行器池中的另一個執行緒中非同步執行轉換函數。
這是說明thenApply()
程式碼片段:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<String> thenApplyResultFuture = future.thenApply(num -> "Result: " + num);
String thenApplyResult = thenApplyResultFuture.join();
assertEquals("Result: 5", thenApplyResult);
在此範例中,如果結果已經可用且目前執行緒相容,則thenApply()
可能會同步執行該函數。但是,需要注意的是, CompletableFuture
會根據各種因素(例如結果的可用性和執行緒上下文)智慧地決定是同步執行還是非同步執行。
3.2. thenApplyAsync()
相較之下, thenApplyAsync()
透過利用執行器池(通常是ForkJoinPool.commonPool()
中的執行緒來保證所提供函數的非同步執行。這確保了該函數是非同步執行的,並且可以在單獨的執行緒中運行,從而防止當前執行緒的任何阻塞。
以下是我們如何使用thenApplyAsync()
:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<String> thenApplyAsyncResultFuture = future.thenApplyAsync(num -> "Result: " + num);
String thenApplyAsyncResult = thenApplyAsyncResultFuture.join();
assertEquals("Result: 5", thenApplyAsyncResult);
在此範例中,即使結果立即可用, thenApplyAsync()
也始終安排該函數在單獨的執行緒上非同步執行。
4. 控制線程
雖然thenApply()
和thenApplyAsync()
都啟用非同步轉換,但它們在支援指定自訂執行器並從而控制執行緒方面有所不同。
4.1. thenApply()
thenApply()
方法不直接支援指定自訂執行器來控制執行緒。它依賴CompletableFuture
的預設行為,它可以在完成前一階段的相同線程(通常是來自公共池的線程)上執行轉換函數。
4.2. thenApplyAsync()
相較之下, thenApplyAsync()
允許我們明確指定一個執行器來控制執行緒。透過提供自訂執行器,我們可以指定轉換函數的執行位置,從而實現更精確的執行緒管理。
以下是我們如何將自訂執行器與thenApplyAsync()
一起使用:
ExecutorService customExecutor = Executors.newFixedThreadPool(4);
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 5;
}, customExecutor);
CompletableFuture<String> resultFuture = future.thenApplyAsync(num -> "Result: " + num, customExecutor);
String result = resultFuture.join();
assertEquals("Result: 5", result);
customExecutor.shutdown();
在此範例中,建立了一個固定執行緒池大小為4
的自訂執行器。 thenApplyAsync()
方法然後使用此自訂執行程序,提供對轉換函數的執行緒的控制。
5. 異常處理
thenApply()
和henApplyAsync()
之間異常處理的主要區別在於異常何時以及如何變得可見。
5.1. thenApply()
如果提供給thenApply()
的轉換函數拋出異常,則thenApply()
階段會立即異常地完成CompletableFuture
。此異常完成在CompletionException
中攜帶拋出的異常,包裝原始異常。
讓我們用一個例子來說明這一點:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<String> resultFuture = future.thenApply(num -> "Result: " + num / 0);
assertThrows(CompletionException.class, () -> resultFuture.join());
在這個範例中,我們嘗試將5
除以0
,這會導致拋出ArithmeticException
。此CompletionException
直接傳播到下一個階段或呼叫者,這表示函數內的任何異常都可以立即可見以進行處理。因此,如果我們嘗試使用get()
、 join()
或thenAccept()
等方法存取結果,我們會直接遇到CompletionException
:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<String> resultFuture = future.thenApply(num -> "Result: " + num / 0);
try {
// Accessing the result using join()
String result = resultFuture.join();
assertEquals("Result: 5", result);
} catch (CompletionException e) {
assertEquals("java.lang.ArithmeticException: / by zero", e.getMessage());
}
在此範例中,函數期間引發的異常傳遞給thenApply().
這個階段識別出問題並將原始異常包裝在CompletionException
中,讓我們可以進一步處理它。
5.2. thenApplyAsync()
雖然轉換函數異步運行,但其中的任何異常都不會直接傳播到傳回的CompletableFuture
。當我們呼叫get()
、 join()
或thenAccept()
等方法時,異常不會立即可見。這些方法會阻塞,直到非同步操作完成為止,如果處理不當,可能會導致死鎖。
要處理thenApplyAsync()
中的異常,我們必須使用專用方法,例如handle()
、 exceptionally()
或whenComplete()
。這些方法允許我們在異常發生時攔截並處理非同步發生的異常。
以下是示範使用句柄進行明確處理的程式碼片段:
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<String> thenApplyAsyncResultFuture = future.thenApplyAsync(num -> "Result: " + num / 0);
String result = thenApplyAsyncResultFuture.handle((res, error) -> {
if (error != null) {
// Handle the error appropriately, eg, return a default value
return "Error occurred";
} else {
return res;
}
})
.join(); // Now join() won't throw the exception
assertEquals("Error occurred", result);
在此範例中,即使thenApplyAsync(),
它也不會在resultFuture
中直接可見。 join()
方法會阻塞並最終解開CompletionException
,從而顯示原始的ArithmeticException
。
6. 使用案例
在本節中,我們將探討CompletableFuture
中thenApply()
和thenApplyAsync()
方法的常見使用案例。
6.1. thenApply()
henApply()
方法在以下場景中特別有用:
- 順序轉換:當需要對
CompletableFuture
的結果順序套用轉換時。這可能涉及將數字結果轉換為字串或根據結果執行計算等任務。 - 輕量級操作:它非常適合執行小型、快速的轉換,不會對呼叫執行緒造成明顯的阻塞。範例包括將數字轉換為字串、根據結果執行計算或操作資料結構。
6.2. thenApplyAsync()
另一方面, thenApplyAsync()
方法適用於下列情況:
- 非同步轉換:當需要非同步應用轉換時,可能會利用多個執行緒並行執行。例如,在使用者上傳影像進行編輯的 Web 應用程式中,使用
CompletableFuture
進行非同步轉換有利於同時套用調整大小、濾鏡和浮水印,從而提高處理效率和使用者體驗。 - 阻塞操作:如果轉換函數涉及阻塞操作、I/O 操作或計算密集型任務,則
thenApplyAsync()
會變得有利。透過將此類計算卸載到單獨的線程,有助於防止阻塞呼叫線程,從而確保更流暢的應用程式效能。
七、總結
以下是比較thenApply()
和thenApplyAsync().
特徵 | thenApply() |
thenApplyAsync() |
---|---|---|
執行行為 | 與前一階段相同的線程或來自執行程序池的單獨線程(如果在完成之前調用) | 將執行緒與執行器池分開 |
自訂執行器支援 | 不支援 | 支援線程控制的自訂執行器 |
例外處理 | 立即在CompletionException 中傳播異常 |
不直接可見的異常需要明確處理 |
表現 | 可能會阻塞呼叫線程 | 避免阻塞並提高效能 |
用例 | 順序轉換、輕量級操作 | 非同步轉換、阻塞操作 |
八、結論
在本文中,我們探討了CompletableFuture
框架中thenApply()
和thenApplyAsync()
方法之間的功能和差異。
thenApply()
可能會阻塞線程,使其適合輕量級轉換或可以接受同步執行的場景。另一方面, thenApplyAsync()
保證非同步執行,使其非常適合涉及潛在阻塞或回應能力至關重要的運算密集型任務的操作。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。