從Future到CompletableFuture,Java異步編程進化史
一、引言:異步編程的重要性
在當今數字化時代,電商購物已成為人們生活中不可或缺的一部分。想象一下,你正在開發一個電商系統,當用戶訪問商品詳情頁時,需要同時展示多個商品的信息,包括商品的基本描述、價格、庫存、用戶評價等。這些信息可能來自不同的數據源,如數據庫、緩存或者外部接口,獲取每個商品信息的操作都可能是耗時的 I/O 操作。如果采用傳統的同步編程方式,一個一個地獲取商品信息,那么整個頁面的加載速度將會變得非常緩慢,嚴重影響用戶體驗。
在現代 Java 開發中,類似這樣需要處理大量并發任務和耗時操作的場景越來越多。而異步編程就像是一把神奇的鑰匙,能夠幫助我們高效地解決這些問題,顯著提升程序的性能和響應性。它允許程序在執行一個耗時操作的同時,不阻塞主線程,繼續執行其他任務,從而充分利用 CPU 資源,提高系統的并發處理能力。
今天,我們要深入探討的主角 ——CompletableFuture,正是 Java 異步編程領域中的一位強大利器。它在 Java 8 中被引入,極大地簡化了異步編程的復雜性,為開發者提供了更加便捷、靈活的異步操作方式,讓我們能夠更加優雅地處理異步任務的創建、組合、回調和異常處理等。接下來,就讓我們一起揭開 CompletableFuture 的神秘面紗,領略它的強大魅力吧!
二、Java 異步編程的發展歷程
在 Java 的發展長河中,異步編程的演進是一個不斷突破和創新的過程,它見證了 Java 語言在應對復雜計算場景時的不斷進化。
早期,Java 主要通過 Thread 類和 Runnable 接口來實現異步編程。這種方式為開發者提供了基本的多線程能力,讓程序能夠在多個線程中并行執行任務。例如,在開發一個簡單的文件處理程序時,我們可以創建一個 Thread 類的子類,并重寫其 run 方法來實現文件讀取和處理的邏輯。然后,通過創建該子類的實例并調用 start 方法,就可以啟動一個新的線程來執行文件處理任務,而主線程則可以繼續執行其他操作。
public class FileProcessor extends Thread {private String filePath;public FileProcessor(String filePath) {this.filePath = filePath;}@Overridepublic void run() {// 模擬文件讀取和處理邏輯try {Thread.sleep(2000);System.out.println("文件 " + filePath + " 處理完成");} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {FileProcessor fileProcessor = new FileProcessor("example.txt");fileProcessor.start();System.out.println("主線程繼續執行其他任務");}}
不過,這種方式存在一些明顯的缺點。創建和管理線程的開銷較大,頻繁地創建和銷毀線程會消耗大量的系統資源。當需要處理多個線程之間的協作和同步時,代碼會變得復雜,容易出現死鎖等問題。而且,Thread 類和 Runnable 接口缺乏對異步任務結果的有效管理機制,獲取任務執行結果變得很不方便。
為了解決這些問題,Java 5 引入了 Future 接口,這是 Java 異步編程發展中的一個重要里程碑。Future 接口為異步任務的管理和結果獲取提供了一種標準的方式。通過 Future 接口,我們可以提交一個異步任務,并在需要的時候獲取其執行結果,還能檢查任務的狀態,如是否完成、是否被取消等。比如,在一個電商系統中,計算商品的推薦列表可能是一個耗時的任務,我們可以使用 Future 接口來異步執行這個任務。
import java.util.concurrent.*;public class ProductRecommender {public static void main(String[] args) {ExecutorService executor = Executors.newSingleThreadExecutor();Future<List<String>> future = executor.submit(() -> {// 模擬計算商品推薦列表的耗時操作Thread.sleep(3000);List<String> recommendations = Arrays.asList("商品A", "商品B", "商品C");return recommendations;});// 主線程可以繼續執行其他操作System.out.println("主線程繼續執行其他任務");try {List<String> recommendations = future.get();System.out.println("商品推薦列表: " + recommendations);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();} finally {executor.shutdown();}}}
上述代碼中,首先創建了一個線程池 ExecutorService,并提交了一個 Callable 任務,該任務返回一個 Future 對象。在主線程中,我們可以繼續執行其他操作,然后通過調用 Future 對象的 get 方法來獲取異步任務的執行結果。如果任務尚未完成,get 方法會阻塞當前線程,直到任務完成并返回結果。
Future 接口的出現,使得 Java 的異步編程更加規范和易于管理,極大地提高了程序的并發性能和響應能力。但它也并非完美無缺,在處理復雜的異步任務組合和回調時,Future 接口的局限性逐漸顯現,代碼可能會變得冗長和難以維護。
三、CompletableFuture 全面解析
(一)CompletableFuture 的概述
CompletableFuture 是 Java 8 引入的一個強大的異步編程工具,它極大地簡化了異步編程的復雜性。它實現了 Future 接口,同時還實現了 CompletionStage 接口,這使得它不僅具備了 Future 的基本功能,還提供了更豐富的異步操作和任務編排能力。
與傳統的 Future 相比,CompletableFuture 的最大優勢在于它的非阻塞性和對異步任務結果的靈活處理。在傳統的 Future 中,我們獲取任務結果時通常需要調用get()方法,這個方法會阻塞當前線程,直到任務完成并返回結果。而 CompletableFuture 則通過回調機制,允許我們在任務完成時自動觸發后續操作,無需阻塞線程,從而提高了程序的并發性能和響應速度。此外,CompletableFuture 還支持鏈式調用和函數式編程風格,使得代碼更加簡潔、易讀。
(二)核心特性
- 異步執行任務:CompletableFuture 提供了兩個靜態方法supplyAsync和runAsync來開啟異步任務。supplyAsync方法接受一個Supplier接口的實現,用于返回一個異步計算的結果;runAsync方法接受一個Runnable接口的實現,用于執行一個沒有返回值的異步任務。這兩個方法都有兩個重載版本,一個是使用默認的線程池(ForkJoinPool.commonPool ()),另一個是可以指定自定義的線程池。
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 使用supplyAsync方法開啟一個異步任務,返回一個CompletableFuture對象CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {// 模擬耗時操作try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return "任務1的結果";});// 使用runAsync方法開啟一個異步任務,返回一個CompletableFuture<Void>對象CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {// 模擬耗時操作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("任務2執行完畢");});// 獲取future1的結果,如果任務未完成,get方法會阻塞當前線程String result1 = future1.get();System.out.println("任務1的結果是: " + result1);// 等待future2完成,因為future2沒有返回值,所以不需要調用get方法獲取結果future2.join();}}
在上述代碼中,supplyAsync方法創建了一個異步任務,該任務在后臺線程中執行,模擬了一個耗時 2 秒的操作,并返回一個結果。runAsync方法創建了另一個異步任務,同樣模擬了一個耗時 1 秒的操作,但沒有返回值,只是在任務完成時打印一條消息。主線程在創建這兩個異步任務后,不會被阻塞,可以繼續執行其他操作。當需要獲取supplyAsync任務的結果時,調用get方法,此時如果任務尚未完成,主線程會被阻塞,直到任務完成并返回結果。而對于runAsync任務,由于不需要獲取其返回值,所以可以使用join方法等待任務完成,join方法和get方法類似,但它不會拋出受檢異常。
- 鏈式調用:CompletableFuture 支持鏈式調用,通過thenApply、thenAccept、thenRun等方法,可以對異步任務的結果進行處理,并將處理結果傳遞給下一個階段的任務。這種鏈式調用的方式使得代碼更加簡潔、易讀,并且能夠清晰地表達任務之間的依賴關系。
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureChainDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任務1執行中...");return "任務1的結果";}).thenApply(result -> {System.out.println("任務2執行中,處理任務1的結果: " + result);return result + " 經過任務2的處理";}).thenAccept(finalResult -> {System.out.println("任務3執行中,最終結果是: " + finalResult);});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成Thread.sleep(2000);}}
在這段代碼中,首先使用supplyAsync方法創建了一個異步任務,該任務返回一個結果。然后通過thenApply方法,接收上一個任務的結果,并對其進行處理,返回一個新的結果。最后使用thenAccept方法,接收thenApply處理后的結果,并進行消費,但不返回新的結果。整個過程通過鏈式調用的方式,將三個任務串聯起來,形成了一個異步任務鏈。需要注意的是,由于這些異步任務是在后臺線程中執行的,主線程在創建完任務鏈后,會繼續執行后續代碼。為了讓異步任務有足夠的時間完成,這里使用Thread.sleep(2000)方法讓主線程睡眠 2 秒。
- 組合異步操作:在實際應用中,我們常常需要將多個異步任務組合起來,以實現更復雜的業務邏輯。CompletableFuture 提供了thenCombine、thenAcceptBoth等方法,用于組合多個異步任務。
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureCombineDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("任務A執行中...");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return "任務A的結果";});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {System.out.println("任務B執行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}return "任務B的結果";});CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {System.out.println("組合任務執行中,合并任務A和任務B的結果");return result1 + " 和 " + result2;});String combinedResult = combinedFuture.get();System.out.println("最終組合結果是: " + combinedResult);}}
上述代碼中,首先創建了兩個異步任務future1和future2,分別模擬了耗時 2 秒和 1 秒的操作,并返回各自的結果。然后使用thenCombine方法將這兩個任務組合起來,thenCombine方法接受另一個CompletableFuture對象和一個BiFunction函數,當future1和future2都完成時,會將它們的結果作為參數傳遞給BiFunction函數,該函數對兩個結果進行合并,并返回一個新的結果,這個新的結果會被包裝成一個新的CompletableFuture對象combinedFuture。最后通過get方法獲取combinedFuture的結果,即兩個任務結果的組合。
- 異常處理:在異步編程中,異常處理是非常重要的一環。CompletableFuture 提供了exceptionally和handle方法,用于優雅地處理異步任務中可能出現的異常。
import java.util.concurrent.CompletableFuture;public class CompletableFutureExceptionDemo {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {System.out.println("任務執行中...");if (Math.random() > 0.5) {throw new RuntimeException("任務執行出現異常");}return "任務正常結果";}).exceptionally(ex -> {System.out.println("捕獲到異常: " + ex.getMessage());return "默認結果";}).thenAccept(result -> {System.out.println("最終結果是: " + result);});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
在這段代碼中,supplyAsync方法創建的異步任務中,通過Math.random()方法隨機生成一個數,如果這個數大于 0.5,則拋出一個運行時異常,否則返回正常結果。exceptionally方法用于捕獲異步任務中拋出的異常,當捕獲到異常時,會執行exceptionally方法中的處理邏輯,返回一個默認結果。如果異步任務正常完成,exceptionally方法不會被執行,而是直接執行thenAccept方法,處理正常的結果。同樣,為了讓異步任務有足夠的時間完成,主線程通過Thread.sleep(2000)方法睡眠 2 秒 。
四、代碼示例與實踐
(一)創建異步任務
在使用 CompletableFuture 進行異步編程時,創建異步任務是基礎操作。我們可以使用supplyAsync和runAsync方法來創建異步任務,這兩個方法都有使用默認線程池和自定義線程池的重載版本。
使用默認線程池創建帶有返回值的異步任務,示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureCreateTask {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {// 模擬異步任務中的耗時操作,比如查詢數據庫、調用遠程接口等try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return 42;});// 獲取異步任務的執行結果,如果任務尚未完成,get方法會阻塞當前線程int result = future.get();System.out.println("任務的結果是: " + result);}}
在上述代碼中,CompletableFuture.supplyAsync方法接受一個Supplier接口的實現,以 Lambda 表達式的形式定義了異步任務的邏輯。在這個任務中,通過Thread.sleep(2000)模擬了一個耗時 2 秒的操作,然后返回結果 42。主線程通過future.get()方法獲取異步任務的執行結果,此時如果任務尚未完成,get方法會阻塞主線程,直到任務完成并返回結果。
接下來,我們看一下使用自定義線程池創建異步任務的示例:
import java.util.concurrent.*;public class CompletableFutureCustomThreadPool {public static void main(String[] args) throws ExecutionException, InterruptedException {// 創建一個自定義線程池,設置核心線程數為5,最大線程數為10,線程空閑時間為60秒,任務隊列容量為100ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,60,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {// 模擬異步任務中的耗時操作,比如復雜的計算邏輯try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}return "自定義線程池執行的任務結果";}, executor);// 獲取異步任務的執行結果String result = future.get();System.out.println("任務的結果是: " + result);// 關閉線程池,釋放資源executor.shutdown();}}
在這個示例中,我們首先創建了一個ThreadPoolExecutor類型的自定義線程池,設置了核心線程數、最大線程數、線程空閑時間和任務隊列容量等參數。然后,使用CompletableFuture.supplyAsync方法并傳入自定義線程池,定義了一個異步任務。在任務中同樣模擬了一個耗時 3 秒的操作,并返回結果。最后,在主線程中獲取任務結果,并在任務完成后關閉線程池,以釋放資源。
使用runAsync方法創建沒有返回值的異步任務,使用默認線程池的示例如下:
import java.util.concurrent.CompletableFuture;public class CompletableFutureRunAsyncDefault {public static void main(String[] args) {CompletableFuture.runAsync(() -> {// 模擬異步任務中的操作,比如打印日志、更新緩存等try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("沒有返回值的異步任務執行完成");});System.out.println("主線程繼續執行其他任務");}}
在這段代碼中,CompletableFuture.runAsync方法接受一個Runnable接口的實現,定義了一個沒有返回值的異步任務。任務中通過Thread.sleep(1000)模擬了一個耗時 1 秒的操作,然后打印任務完成的消息。由于runAsync方法創建的任務沒有返回值,所以主線程不需要等待任務完成,可以直接繼續執行后續操作。
使用自定義線程池創建沒有返回值的異步任務示例如下:
import java.util.concurrent.*;public class CompletableFutureRunAsyncCustom {public static void main(String[] args) {ThreadPoolExecutor executor = new ThreadPoolExecutor(3,6,30,TimeUnit.SECONDS,new LinkedBlockingQueue<>(50));CompletableFuture.runAsync(() -> {// 模擬異步任務中的操作,比如發送郵件、調用第三方接口等try {Thread.sleep(1500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("使用自定義線程池的無返回值異步任務執行完成");}, executor);System.out.println("主線程繼續執行其他任務");// 關閉線程池,釋放資源executor.shutdown();}}
此示例中,我們創建了一個自定義線程池,并使用CompletableFuture.runAsync方法結合自定義線程池創建了一個沒有返回值的異步任務。任務執行完畢后,打印相應的消息,主線程繼續執行其他操作,最后關閉線程池。
(二)鏈式調用與結果處理
CompletableFuture 的鏈式調用功能使得我們可以對異步任務的結果進行一系列的處理,讓代碼更加簡潔和易讀。通過thenApply、thenAccept、thenRun等方法,我們可以實現對異步任務結果的轉換、消費和后續操作。
thenApply方法用于對異步任務的結果進行轉換,它接收一個Function函數,將前一個任務的結果作為參數傳入該函數,并返回一個新的結果。示例代碼如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenApply {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任務1執行中...");return 10;}).thenApply(result -> {System.out.println("任務2執行中,處理任務1的結果: " + result);return result * 2;}).thenApply(finalResult -> {System.out.println("任務3執行中,處理任務2的結果: " + finalResult);return finalResult + 5;}).thenAccept(result -> {System.out.println("最終結果是: " + result);});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成Thread.sleep(2000);}}
在上述代碼中,首先使用supplyAsync方法創建了一個異步任務,返回值為 10。然后通過thenApply方法,將任務 1 的結果乘以 2,得到新的結果 20。接著再次使用thenApply方法,將 20 加上 5,得到最終結果 25。最后通過thenAccept方法,消費最終結果并打印輸出。整個過程通過鏈式調用,將多個異步任務串聯起來,實現了對結果的逐步處理。
thenAccept方法用于消費異步任務的結果,它接收一個Consumer函數,將前一個任務的結果作為參數傳入該函數,但不返回新的結果。示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenAccept {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任務1執行中...");return "Hello";}).thenAccept(result -> {System.out.println("任務2執行中,消費任務1的結果: " + result);// 可以在這個方法中進行一些基于結果的操作,比如打印日志、更新數據庫等});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成Thread.sleep(1000);}}
在這個例子中,supplyAsync方法創建的異步任務返回字符串 "Hello"。thenAccept方法接收這個結果,并在其內部消費該結果,打印出消費信息。在thenAccept方法中,我們可以根據業務需求進行一些基于結果的操作,比如將結果寫入日志文件、更新數據庫記錄等。
thenRun方法則是在異步任務完成后執行一個無參數的操作,它不關心前一個任務的結果。示例代碼如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenRun {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture.supplyAsync(() -> {System.out.println("任務1執行中...");return 100;}).thenRun(() -> {System.out.println("任務2執行中,不依賴任務1的結果");// 這里可以執行一些與前一個任務結果無關的操作,比如發送通知等});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成Thread.sleep(1000);}}
在這段代碼中,supplyAsync方法創建的異步任務返回值為 100,但thenRun方法并不關心這個結果,它只是在任務 1 完成后執行一個獨立的操作,打印出相應的消息。在實際應用中,thenRun方法可以用于執行一些與前一個任務結果無關的操作,比如發送郵件通知、更新系統狀態等。
(三)組合異步操作
在實際的開發場景中,我們常常需要將多個異步任務組合起來,以實現更復雜的業務邏輯。CompletableFuture 提供了thenCombine、thenAcceptBoth等方法,方便我們對多個異步任務進行組合操作。
thenCombine方法用于將兩個異步任務的結果進行合并,它接收另一個CompletableFuture對象和一個BiFunction函數。當兩個異步任務都完成時,BiFunction函數會將兩個任務的結果作為參數進行處理,并返回一個新的結果。示例代碼如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenCombine {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("任務A執行中...");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return 10;});CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {System.out.println("任務B執行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}return 20;});CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {System.out.println("組合任務執行中,合并任務A和任務B的結果");return result1 + result2;});int combinedResult = combinedFuture.get();System.out.println("最終組合結果是: " + combinedResult);}}
在上述代碼中,首先創建了兩個異步任務future1和future2,分別模擬了耗時 2 秒和 1 秒的操作,并返回各自的結果 10 和 20。然后使用thenCombine方法將這兩個任務組合起來,當future1和future2都完成時,BiFunction函數會將它們的結果相加,得到新的結果 30。最后通過get方法獲取組合任務的結果并打印輸出。
thenAcceptBoth方法則是在兩個異步任務都完成后,將它們的結果作為參數傳遞給一個BiConsumer函數進行消費,但不返回新的結果。示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureThenAcceptBoth {public static void main(String[] args) throws ExecutionException, InterruptedException {CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {System.out.println("任務X執行中...");try {Thread.sleep(1500);} catch (InterruptedException e) {e.printStackTrace();}return "Hello";});CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {System.out.println("任務Y執行中...");try {Thread.sleep(2500);} catch (InterruptedException e) {e.printStackTrace();}return "World";});future1.thenAcceptBoth(future2, (result1, result2) -> {System.out.println("組合任務執行中,消費任務X和任務Y的結果");System.out.println("拼接后的結果是: " + result1 + " " + result2);});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成Thread.sleep(3000);}}
在這個例子中,future1和future2分別是兩個異步任務,完成后分別返回 "Hello" 和 "World"。thenAcceptBoth方法接收這兩個任務的結果,并通過BiConsumer函數將它們拼接起來并打印輸出。在thenAcceptBoth方法中,我們可以根據業務需求對兩個任務的結果進行各種消費操作,比如將結果寫入文件、發送到消息隊列等。
(四)異常處理
在異步編程中,異常處理是至關重要的環節。CompletableFuture 提供了exceptionally和handle方法,幫助我們優雅地處理異步任務中可能出現的異常。
exceptionally方法用于捕獲異步任務執行過程中拋出的異常,并返回一個默認值或處理后的結果。示例代碼如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureExceptionally {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {System.out.println("任務執行中...");if (Math.random() > 0.5) {throw new RuntimeException("任務執行出現異常");}return "任務正常結果";}).exceptionally(ex -> {System.out.println("捕獲到異常: " + ex.getMessage());return "默認結果";}).thenAccept(result -> {System.out.println("最終結果是: " + result);});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
在上述代碼中,supplyAsync方法創建的異步任務中,通過Math.random()方法隨機生成一個數,如果這個數大于 0.5,則拋出一個運行時異常,否則返回正常結果。exceptionally方法用于捕獲異步任務中拋出的異常,當捕獲到異常時,會執行exceptionally方法中的處理邏輯,打印異常信息并返回一個默認結果。如果異步任務正常完成,exceptionally方法不會被執行,而是直接執行thenAccept方法,處理正常的結果。
handle方法則更加靈活,它不僅可以處理異常,還可以處理正常的計算結果。handle方法接收一個BiFunction函數,該函數接收兩個參數:異步任務的結果和可能出現的異常。根據這兩個參數,我們可以返回不同的處理結果。示例如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;public class CompletableFutureHandle {public static void main(String[] args) {CompletableFuture.supplyAsync(() -> {System.out.println("任務執行中...");if (Math.random() > 0.3) {throw new RuntimeException("任務執行出現異常");}return "任務正常結果";}).handle((result, ex) -> {if (ex!= null) {System.out.println("捕獲到異常: " + ex.getMessage());return "異常處理后的結果";}System.out.println("任務正常完成,結果是: " + result);return result;}).thenAccept(finalResult -> {System.out.println("最終結果是: " + finalResult);});// 主線程需要等待一段時間,以便異步任務有足夠的時間完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
在這個例子中,supplyAsync方法創建的異步任務同樣可能會拋出異常。handle方法中的BiFunction函數會根據任務的執行情況進行處理。如果出現異常,會打印異常信息并返回異常處理后的結果;如果任務正常完成,會打印正常結果并返回該結果。最后通過thenAccept方法處理最終的
五、與其他異步編程方式的對比
(一)與 Future 的對比
在 Java 的異步編程領域,Future 接口是早期的重要工具,而 CompletableFuture 則是在其基礎上的進一步發展和增強。下面我們從功能、性能、靈活性等方面來對比這兩者,看看 CompletableFuture 究竟有哪些獨特的優勢。
從功能層面來看,Future 主要用于表示異步計算的結果,它提供了isDone方法用于檢查計算是否完成,get方法用于獲取計算結果,以及cancel方法用于取消計算任務。然而,Future 的功能相對較為單一,獲取結果時,如果計算尚未完成,get方法會阻塞當前線程,這在一定程度上限制了其在復雜異步場景中的應用。例如,在一個需要同時處理多個異步任務并對結果進行復雜處理的電商系統中,使用 Future 可能會導致主線程長時間阻塞,影響系統的響應性能。
相比之下,CompletableFuture 的功能要豐富得多。它不僅實現了 Future 接口,具備 Future 的基本功能,還提供了大量用于異步任務編排和結果處理的方法。通過thenApply、thenAccept、thenRun等方法,我們可以方便地對異步任務的結果進行轉換、消費和后續操作,實現鏈式調用,使代碼更加簡潔和易讀。在處理多個異步任務時,CompletableFuture 提供了thenCombine、thenAcceptBoth、allOf、anyOf等方法,能夠輕松地實現任務的組合和并行執行,滿足各種復雜的業務需求。
在性能方面,由于 Future 的get方法會阻塞線程,當有大量異步任務需要處理時,可能會導致線程資源的浪費和系統性能的下降。而 CompletableFuture 采用了非阻塞的方式來處理異步結果,通過回調機制,在任務完成時自動觸發后續操作,無需阻塞線程,從而提高了系統的并發性能和響應速度。例如,在一個高并發的網絡爬蟲系統中,使用 CompletableFuture 可以充分利用線程資源,同時處理多個網頁的抓取和解析任務,大大提高了爬蟲的效率。
靈活性上,Future 的操作相對較為固定,缺乏對多個異步操作的組合能力。而 CompletableFuture 則提供了強大的組合和處理能力,可以方便地處理復雜的異步任務。它支持在任務執行過程中動態地添加回調函數,根據任務的執行結果或異常情況進行不同的處理,使得異步編程更加靈活和可控。在一個分布式系統中,可能需要調用多個不同的服務來完成一個業務流程,使用 CompletableFuture 可以輕松地將這些異步調用組合起來,實現高效的分布式事務處理。
(二)與其他異步框架的對比
除了與 Future 進行對比,我們也來簡要提及一下 CompletableFuture 與其他異步框架的差異,比如 Guava 的 ListenableFuture。
Guava 的 ListenableFuture 是對 Java 原生 Future 的擴展,它提供了一種可以監聽異步任務完成的機制,通過添加回調函數,在任務完成時自動觸發回調操作,這在一定程度上彌補了 Future 的不足。例如,在一個需要實時處理消息的系統中,使用 ListenableFuture 可以在消息處理完成后立即觸發通知操作,提高系統的實時性。
然而,CompletableFuture 作為 Java 標準庫的一部分,在使用和集成上具有獨特的便利性。它無需額外引入第三方庫,減少了項目的依賴,降低了維護成本。而且,CompletableFuture 與 Java 8 引入的 Lambda 表達式、Stream API 等新特性緊密結合,能夠更好地利用 Java 語言的新特性進行異步編程,使得代碼更加簡潔、高效。在使用 CompletableFuture 時,我們可以直接使用 Lambda 表達式來定義異步任務和回調函數,利用 Stream API 對多個異步任務進行并行處理和結果匯總,大大提高了開發效率。
在功能方面,CompletableFuture 同樣具有一定的優勢。它提供了更豐富的方法來處理異步任務的組合、異常處理和超時控制等。在處理多個異步任務的組合時,CompletableFuture 的thenCombine、allOf、anyOf等方法比 ListenableFuture 更加簡潔和靈活,能夠更方便地實現復雜的業務邏輯。在異常處理方面,CompletableFuture 的exceptionally和handle方法提供了更強大的異常處理能力,能夠更優雅地處理異步任務中可能出現的異常情況。
六、實際應用場景
(一)電商系統中的商品比價
在電商領域,用戶在購買商品時往往希望能夠快速了解不同平臺上同款商品的價格,以便做出最優的購買決策。為了實現這一功能,我們可以利用 CompletableFuture 來異步地從多個電商平臺獲取商品價格信息,并進行比價展示。
假設我們有一個電商比價系統,需要從淘寶、京東、拼多多等多個電商平臺獲取某款手機的價格。使用 CompletableFuture,我們可以將每個平臺的價格查詢操作封裝成一個異步任務,然后并行地執行這些任務,最后將所有平臺的價格信息匯總展示給用戶。示例代碼如下:
import java.util.ArrayList;import java.util.List;import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.stream.Collectors;// 定義電商平臺類class EcommercePlatform {private String platformName;public EcommercePlatform(String platformName) {this.platformName = platformName;}// 模擬從電商平臺獲取商品價格的方法public double getProductPrice(String product) {// 這里可以替換為實際的價格查詢邏輯,比如調用電商平臺的APItry {Thread.sleep(1000); // 模擬網絡延遲或數據查詢耗時} catch (InterruptedException e) {e.printStackTrace();}// 隨機生成一個價格作為示例return Math.random() * 1000 + 1000;}}public class PriceComparisonSystem {public static void main(String[] args) throws ExecutionException, InterruptedException {// 創建線程池ExecutorService executor = Executors.newFixedThreadPool(3);// 定義要查詢價格的商品String product = "iPhone 15";// 定義各個電商平臺EcommercePlatform taobao = new EcommercePlatform("淘寶");EcommercePlatform jd = new EcommercePlatform("京東");EcommercePlatform pinduoduo = new EcommercePlatform("拼多多");// 使用CompletableFuture異步獲取各個平臺的商品價格CompletableFuture<Double> taobaoPriceFuture = CompletableFuture.supplyAsync(() -> taobao.getProductPrice(product), executor);CompletableFuture<Double> jdPriceFuture = CompletableFuture.supplyAsync(() -> jd.getProductPrice(product), executor);CompletableFuture<Double> pinduoduoPriceFuture = CompletableFuture.supplyAsync(() -> pinduoduo.getProductPrice(product), executor);// 將所有的CompletableFuture放入一個列表中List<CompletableFuture<Double>> futureList = new ArrayList<>();futureList.add(taobaoPriceFuture);futureList.add(jdPriceFuture);futureList.add(pinduoduoPriceFuture);// 使用CompletableFuture.allOf等待所有異步任務完成CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join();// 獲取各個平臺的商品價格List<Double> priceList = futureList.stream().map(CompletableFuture::join).collect(Collectors.toList());// 打印各個平臺的商品價格System.out.println("商品 " + product + " 在不同平臺的價格如下:");System.out.println("淘寶:" + priceList.get(0));System.out.println("京東:" + priceList.get(1));System.out.println("拼多多:" + priceList.get(2));// 關閉線程池executor.shutdown();}}
在上述代碼中,首先創建了一個固定大小的線程池,用于執行異步任務。然后,定義了三個電商平臺對象,并使用 CompletableFuture 的supplyAsync方法將每個平臺的價格查詢操作封裝成異步任務,這些任務將在后臺線程中并行執行。通過CompletableFuture.allOf方法等待所有異步任務完成,最后獲取各個平臺的商品價格并打印輸出。使用 CompletableFuture 實現商品比價功能,大大提高了獲取價格信息的效率,避免了因順序查詢而導致的長時間等待,為用戶提供了更快速、便捷的購物體驗。
(二)多數據源的數據獲取與整合
在企業級開發中,經常會遇到需要從多個數據源獲取數據并進行整合處理的場景。例如,在一個大型企業的數據分析系統中,用戶信息可能存儲在關系型數據庫中,而用戶的行為數據則存儲在分布式緩存和日志文件中。為了生成全面的用戶分析報告,我們需要從這些不同的數據源獲取數據,并進行整合和分析。
假設我們要開發一個用戶數據分析模塊,需要從數據庫中獲取用戶的基本信息,從緩存中獲取用戶的近期登錄記錄,從日志文件中獲取用戶的操作行為數據。使用 CompletableFuture,我們可以將這些數據獲取操作并行化,提高數據獲取的效率。示例代碼如下:
import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;// 模擬數據庫操作類class DatabaseService {public String getUserInfo(String userId) {try {Thread.sleep(1500); // 模擬數據庫查詢耗時} catch (InterruptedException e) {e.printStackTrace();}return "用戶基本信息:姓名:張三,年齡:25,性別:男";}}// 模擬緩存操作類class CacheService {public String getLoginRecords(String userId) {try {Thread.sleep(1000); // 模擬緩存查詢耗時} catch (InterruptedException e) {e.printStackTrace();}return "近期登錄記錄:[2024-01-01 10:00:00,2024-01-02 15:30:00]";}}// 模擬日志文件操作類class LogService {public String getOperationLogs(String userId) {try {Thread.sleep(2000); // 模擬日志文件讀取耗時} catch (InterruptedException e) {e.printStackTrace();}return "操作行為數據:[查看商品詳情,添加購物車,提交訂單]";}}public class UserDataAnalysis {public static void main(String[] args) throws ExecutionException, InterruptedException {String userId = "123456";DatabaseService databaseService = new DatabaseService();CacheService cacheService = new CacheService();LogService logService = new LogService();// 使用CompletableFuture異步獲取用戶基本信息CompletableFuture<String> userInfoFuture = CompletableFuture.supplyAsync(() -> databaseService.getUserInfo(userId));// 使用CompletableFuture異步獲取用戶近期登錄記錄CompletableFuture<String> loginRecordsFuture = CompletableFuture.supplyAsync(() -> cacheService.getLoginRecords(userId));// 使用CompletableFuture異步獲取用戶操作行為數據CompletableFuture<String> operationLogsFuture = CompletableFuture.supplyAsync(() -> logService.getOperationLogs(userId));// 使用CompletableFuture.allOf等待所有異步任務完成CompletableFuture.allOf(userInfoFuture, loginRecordsFuture, operationLogsFuture).join();// 獲取并整合數據String userInfo = userInfoFuture.get();String loginRecords = loginRecordsFuture.get();String operationLogs = operationLogsFuture.get();String combinedData = "用戶ID:" + userId + "\n" + userInfo + "\n" + loginRecords + "\n" + operationLogs;System.out.println(combinedData);}}
在這個示例中,分別創建了模擬數據庫、緩存和日志文件操作的類,并定義了相應的數據獲取方法。使用 CompletableFuture 的supplyAsync方法將從不同數據源獲取數據的操作封裝成異步任務,這些任務會在后臺線程中并行執行。通過CompletableFuture.allOf方法等待所有異步任務完成,然后獲取各個數據源的數據并進行整合,最終輸出完整的用戶分析數據。這種方式充分利用了 CompletableFuture 的異步和并行處理能力,大大提高了數據獲取和整合的效率,為企業級應用的高效運行提供了有力支持 。
七、總結與展望
在 Java 異步編程的廣闊領域中,CompletableFuture 無疑占據著舉足輕重的地位。它的出現,為開發者們提供了一種高效、便捷且靈活的異步編程解決方案,極大地簡化了異步任務的處理流程。
從基本的異步任務創建,到復雜的任務組合與結果處理,再到優雅的異常處理機制,CompletableFuture 憑借其豐富的方法和強大的功能,滿足了各種場景下的異步編程需求。與傳統的異步編程方式相比,它的優勢顯而易見,無論是在提升代碼的可讀性、可維護性,還是在提高程序的性能和響應性方面,都表現出色。
在實際項目中,我們已經看到 CompletableFuture 在電商系統、企業級數據處理等諸多領域發揮了重要作用。它能夠幫助我們更高效地利用系統資源,提升用戶體驗,增強系統的競爭力。
展望未來,隨著 Java 技術的不斷發展和應用場景的日益豐富,相信 CompletableFuture 還將不斷進化和完善,為我們帶來更多的驚喜和便利。同時,也希望廣大讀者能夠在日常的開發工作中,大膽地使用 CompletableFuture,不斷探索它的更多可能性,讓我們的程序在異步編程的加持下,運行得更加高效、流暢 。