1. 啟動非同步任務:supplyAsync 與 runAsync
啟動非同步任務最常見的方式是使用 CompletableFuture.supplyAsync。此方法接收會回傳結果的 lambda 或方法。例如,我們想要模擬從伺服器載入資料:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模擬耗時操作(例如檔案下載)
sleep(1000);
return "來自伺服器的資料";
});
System.out.println("任務已啟動!");
// ... 任務執行時,可以在這裡做其他事
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync:當不需要結果時
如果你的任務不回傳任何值(例如只是寫入日誌、發送通知),請使用 runAsync:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("操作完成!");
});
runAsync 一律回傳 CompletableFuture<Void>,因為不期望有結果。
2. thenApply、thenAccept、thenRun:有什麼差別?
當非同步任務完成後,通常會想對結果做點什麼。為此提供了「處理器」方法:
- thenApply — 轉換結果並回傳新結果。
- thenAccept — 接收結果,但不回傳任何值(用於副作用)。
- thenRun — 不接收結果,也不回傳任何值(僅在任務完成後執行一段動作)。
thenApply:處理並轉換結果
若需要 轉換 前一個任務的結果,就使用 thenApply。例如,下載到一個字串,現在想知道它的長度:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("正在計算字串長度...");
return s.length();
});
// 現在 lengthFuture 含有 Integer — 字串 "Java" 的長度
lengthFuture.thenAccept(len -> System.out.println("長度:" + len));
發生了什麼:
- future 含有字串 "Java"。
- thenApply 將字串轉為其長度(int)。
- thenAccept 輸出結果。
thenAccept:對結果執行動作(不回傳任何值)
如果只需要對結果做點事(例如印到螢幕),而不需要回傳任何值,請使用 thenAccept:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "你好,世界!");
future.thenAccept(result -> {
System.out.println("結果:" + result);
});
thenAccept 就像「消費者」:它消耗結果並做一些有用的事。
thenRun:不需要結果的動作
如果你只想在任務完成後執行某個動作,但結果不需要,請使用 thenRun:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "完成!");
future.thenRun(() -> {
System.out.println("載入完成!");
});
請注意:在 thenRun 之中無法取得前一個任務的結果——它會被忽略。
3. 連鎖呼叫:把任務串成處理管線
CompletableFuture 的最大威力在於能建立計算的鏈結。每個方法(thenApply、thenAccept、thenRun)都會回傳新的 CompletableFuture,你可以再對它加上處理器。
範例:多階段處理
讓我們完善一下應用:先載入資料、轉換它、輸出結果,最後寫入日誌表示一切完成。
CompletableFuture.supplyAsync(() -> {
System.out.println("步驟 1:正在載入資料...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("步驟 2:正在轉換資料...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("步驟 3:輸出結果:" + result);
})
.thenRun(() -> {
System.out.println("步驟 4:全部完成!");
});
主控台輸出:
步驟 1:正在載入資料...
步驟 2:正在轉換資料...
步驟 3:輸出結果:JAVA
步驟 4:全部完成!
請注意:
每個下一步都只會在前一步完成後才開始。這讓我們可以建立真正的資料處理「管線」。
4. 非同步變體:thenApplyAsync、thenAcceptAsync、thenRunAsync
預設情況下,處理器(thenApply、thenAccept、thenRun)會在前一個任務完成的同一個執行緒中執行。有時這並不理想——若處理工作很耗資源,最好把它丟到另一個執行緒。
為此有對應的非同步版本:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
有什麼差別?
- 沒有 Async: 處理器可能在與前一個任務相同的執行緒中執行(例如任務在 ForkJoinPool 完成,處理器也可能在那裡)。
- 使用 Async: 處理器保證會在 ForkJoinPool(或你的 Executor)中的另一個執行緒執行。
範例:比較同步與非同步處理器
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("載入中... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
典型輸出:
載入中... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
結論:
非同步處理器會在不同的執行緒中執行。
何時使用 Async 方法?
- 當處理很耗資源(例如繁重計算、網路存取)。
- 當你不想阻塞前一個任務完成所在的執行緒。
- 當你想明確管理執行緒(例如作為第二個參數傳入自訂的 Executor)。
5. 實用細節
表格:thenApply、thenAccept、thenRun 的比較
| 方法 | 會使用結果? | 會回傳值? | 適用情境 |
|---|---|---|---|
|
是 | 是 | 轉換結果 |
|
是 | 否 | 副作用(輸出、記錄日誌) |
|
否 | 否 | 在任務完成後執行一段動作 |
|
是 | 是 | 相同,但在另一個執行緒中 |
|
是 | 否 | 相同,但在另一個執行緒中 |
|
否 | 否 | 相同,但在另一個執行緒中 |
問題:長鏈要怎麼寫?
可以像拼 LEGO 一樣把方法一個接一個串起來:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("結果:" + x));
輸出:
結果:84
每一步都會取得前一步的結果,可以修改它或僅僅使用它。
6. 使用 thenApply、thenAccept、thenRun 時的常見錯誤
錯誤 1:混淆回傳型別。
thenApply 必須回傳一個會傳遞到鏈中下一步的值。如果你誤用 thenApply 卻沒有回傳結果,下一個操作會拿到 null(或甚至無法編譯)。需要副作用時請使用 thenAccept 或 thenRun。
錯誤 2:嘗試在 thenRun 內使用結果。
在 thenRun 內部無法存取前一個任務的結果。若要使用結果,請選擇 thenApply 或 thenAccept。
錯誤 3:阻塞主執行緒。
如果你在主執行緒呼叫 get() 或 join(),就失去了非同步的所有優勢:執行緒會等待任務結束,就像傳統的同步程式碼一樣。最好使用不阻塞的鏈結與回呼。
錯誤 4:未處理例外。
如果在鏈結中發生例外,而你沒有加入處理器(exceptionally、handle、whenComplete),它可能「悄悄」失敗,任務會以錯誤結束而你看不到。務必在鏈結中處理錯誤。
錯誤 5:意外地在其他執行緒中執行。
非同步方法(thenApplyAsync 等)可能在不同的執行緒執行。如果你存取未針對多執行緒保護的變數,可能會發生資料競爭。
GO TO FULL VERSION