CodeGym /課程 /JAVA 25 SELF /CompletableFuture 簡介

CompletableFuture 簡介

JAVA 25 SELF
等級 55 , 課堂 0
開放

1. 同步程式碼的問題

想像一下:你有個程式需要從網路下載資料,或讀取一個很大的檔案。你會寫出類似這樣的程式碼:

String data = readFromFile("bigfile.txt");
System.out.println("資料: " + data);

一切看起來沒問題,但如果檔案很大或網路很慢,程式就會在讀取那一行上面被 卡住。使用者看到的是「當掉」的介面,伺服器無法處理其他請求,而程式設計師……只能嘆氣。

這種情況稱為 阻塞:執行緒(例如應用程式的主執行緒)被迫等待操作完成。而如果這類操作很多——那就準備好面對延遲與低效能吧。

這就好比你走進咖啡店下了單,然後……必須一直站在吧檯前等咖啡做好。其他客人也排在你後面一起等,直到咖啡師先把你的處理完。效率很差,對吧?

非同步:如何拯救世界

非同步程式設計是一種作法:把耗時的操作(例如讀檔、對伺服器發送請求、存取資料庫)放在背景執行緒執行,而主執行緒持續工作:服務使用者、接收新的請求、回應事件。

也就是說,你先下單(啟動任務),然後去忙你的;當咖啡好了(任務完成),只會告訴你:「好了!」

在 Java 出現 CompletableFuture 之前,這件事並不方便。讓我們看看演進過程。

2. 歷史做法:Future 及其限制

在 Java 5 引入了介面 Future —— 第一次嘗試讓非同步任務好用一點。它允許把任務交給執行緒池,並在未來某個時刻取得結果。

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> 2 + 2);
int result = future.get(); // 注意:在任務完成之前,執行緒會被阻塞!

理念聽起來不錯,但實務上發現 Future 很像老舊的郵筒:信寄出去了,但想知道有沒有回覆,就得一直打開往裡看。

它不會在結果就緒時通知你,也不支援像「先做這個,再做那個」的動作鏈,也無法優雅地處理錯誤。最後一切又回到那個會阻塞的 get() 呼叫上,讓非同步重新變成等待。

3. CompletableFuture 的出現:非同步的新風格

在 Java 8,取代過時的 Future 的非同步英雄 —— CompletableFuture —— 登場。這個來自 java.util.concurrent 的類,成為不想手動等待、希望把非同步程式碼寫得優雅精煉的人們的萬用工具。

CompletableFuture 幾乎無所不能。它能在其他執行緒啟動任務,把它們串成鏈——例如先計算結果、再處理、然後再做別的。它也能輕鬆組合多個任務:可以等待全部一起完成,或只等第一個完成的。錯誤處理也很優雅——不必到處寫 try-catch。整體風格更接近函式式:不再是枯燥的呼叫與等待,而是使用具表達力的方法如 thenApplythenAccept 等。

flowchart LR
    A[非同步啟動任務] --> B[處理結果]
    B --> C[下一步操作]
    C --> D[錯誤處理]

就這樣,CompletableFuture 把非同步從難以駕馭的苦工,變成方便又彈性的工具,讓程式碼終於能自由呼吸。

4. 最簡範例:踏入 CompletableFuture 的第一步

來看一個最小的非同步任務範例:

import java.util.concurrent.CompletableFuture;

public class AsyncDemo {
    public static void main(String[] args) {
        // 以非同步方式啟動任務
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2);

        // 取得結果(會阻塞執行緒!)
        try {
            int result = future.get();
            System.out.println("結果: " + result); // 4
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這段程式碼已經在另一個執行緒執行計算——在啟動任務時主執行緒不會被阻塞。不過,一旦呼叫 get(),在結果準備好之前仍然會阻塞執行緒。

那要如何「不」阻塞執行緒?

很簡單:使用在任務完成時會被呼叫的回呼方法:

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2);

future.thenAccept(result -> System.out.println("結果: " + result));
System.out.println("我沒有被阻塞,還可以繼續做其他事情!");

結論:

  • thenAccept —— 就是「訂閱結果」:當任務完成時,呼叫這段程式碼。
  • 主執行緒不等待任務完成,而是繼續運作。

視覺化(事件的偽碼)

[主執行緒] --> [啟動任務]
     |                   |
     v                   v
[做其他事]         [背景執行緒計算 2+2]
     |                   |
     v                   v
[輸出 "我沒有被阻塞..."]
     |                   |
     v                   v
                 [計算完成時 — 觸發 thenAccept]

5. 在應用程式中會是什麼樣子?

想像你在開發一個主控台應用程式,使用者可以要求載入資料(例如來自資料庫或伺服器),而在資料載入期間——程式不會「當掉」,而是持續接受指令。

範例:模擬耗時的操作

import java.util.concurrent.CompletableFuture;

public class AsyncApp {
    public static void main(String[] args) {
        System.out.println("開始載入資料...");

        CompletableFuture<String> dataFuture = CompletableFuture.supplyAsync(() -> {
            // 模擬耗時的載入
            try {
                Thread.sleep(2000); // 2 秒
            } catch (InterruptedException e) {
                return "載入發生錯誤";
            }
            return "資料載入成功!";
        });

        // 訂閱結果
        dataFuture.thenAccept(result -> System.out.println("結果: " + result));

        // 程式持續運作
        System.out.println("在資料載入的同時,我可以做別的事!");

        // 為了避免程式過早結束(僅示範用!)
        try {
            Thread.sleep(2500);
        } catch (InterruptedException ignored) {}
    }
}

主控台輸出會看到:

開始載入資料...
在資料載入的同時,我可以做別的事!
[約 2 秒後]
結果: 資料載入成功!

6. 實用細節

關於底層執行緒的一點說明

當你寫 CompletableFuture.supplyAsync(...) 時,任務預設會在所謂的 ForkJoinPool 中執行——這是 Java 用於平行任務的特殊執行緒池。如果你需要更多控制(例如使用自己的 ExecutorService),可以把它當作第二個參數傳入:

ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 2 + 2, executor);

不過對於簡單任務,標準的執行緒池就足夠了。

取得結果:get(), join(), thenAccept

  • get() —— 會阻塞執行緒直到結果就緒(丟出受檢例外)。
  • join() —— 也會阻塞,但丟出未受檢例外(RuntimeException)。
  • thenAccept()thenApply() 等 —— 不阻塞;當結果就緒時呼叫你提供的函式。

在實際的非同步應用程式中,請盡量避免 get()/join() 在主執行緒中使用!

7. 剛開始使用 CompletableFuture 常見的錯誤

錯誤第 1 點:在主執行緒中使用 get() 或 join().
這樣做會再次把程式阻塞,失去非同步的優勢。改用 thenAcceptthenApply 等方法來處理結果。

錯誤第 2 點:忘了處理錯誤。
如果非同步任務發生例外,它不會「冒泡」到主執行緒。若沒有透過 exceptionallyhandle 處理,你根本不會知道出了什麼事。

錯誤第 3 點:沒有等到程式結束。
在示範程式裡,常得用 Thread.sleep 稍微「拖住」 main 執行緒——否則程式可能比任務更早結束。在實際應用(例如 web 伺服器)通常不是問題,但在主控台示範時要記得這點。

錯誤第 4 點:混淆 thenAccept 與 thenApply。
thenAccept —— 用於「副作用」動作(不回傳值),thenApply —— 用於轉換結果(回傳新的結果)。

錯誤第 5 點:不必要地混用非同步與同步程式碼。
若已經走非同步,就不要再透過 get()/join() 把流程拉回同步,除非是不得已(例如在測試中)。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION