CodeGym /課程 /JAVA 25 SELF /Structured Concurrency

Structured Concurrency

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

1. 簡介

當執行緒各自為政

一般的多執行緒常常像沒有指揮的排練。每個執行緒就像不聽彼此的樂手,各自演奏自己的旋律。有人提早結束去抽菸,有人卡在同一個和弦,有人甚至彈錯音而丟出錯誤。結果不是交響樂,而是刺耳的雜音:很難弄清楚誰在哪裡出錯,更別說要一次把大家都停下來——這根本是場硬仗。

Structured Concurrency 解決的正是這個問題。它把零散的執行緒變成真正的合奏:所有任務都在同一位「指揮」之下。只要指揮下令停止——整個樂團就安靜。如果有一位樂手搞砸了——其他人會被妥善停止,而不會破壞整體和諧。所有結果與錯誤都集中收集,而不是散落在程式碼各處。

想像一下:你不再讓樂手各自亂演,而是把他們集合到同一個演奏廳。有指揮、有譜面,即使小號走音——樂團也不會崩盤,而是體面地結束演出。

Structured Concurrency 帶來什麼

  • 單一「scope」的任務:所有子任務都活在同一段程式碼區塊中,它們的生命週期受此區塊限制。
  • 可預測的結束:父執行緒在所有子任務結束前不會結束。
  • 集中式取消:只要某個任務失敗,或父方決定結束——所有子任務都會被正確取消。
  • 一致性的錯誤處理:子任務的錯誤會被彙整,你可以取得「原因樹」(tree of causes)。
  • 乾淨且可讀的程式碼:沒有「懸掛」的執行緒,沒有被遺忘的任務,也沒有競相取消的混亂。

Structured Concurrency 不只是新的 API,而是一種新的思維模式:任務應當像一般的程式碼區塊一樣被結構化(例如 try-with-resources)。

2. Java 中 Structured Concurrency 的狀態

在撰寫本課程時,Structured Concurrency 的狀態是 Preview(Java 21–23),但預期會在 Java 24/25 推進到 GA(General Availability)。API 位於套件 jdk.incubator.concurrent。在投入生產前,請務必查閱你所用 JDK 版本的最新發行說明!

主要類別:

  • StructuredTaskScope —— 用於管理一組任務的基礎類別。
  • 變體:StructuredTaskScope.ShutdownOnFailureStructuredTaskScope.ShutdownOnSuccess —— 任務的結束策略。

StructuredTaskScope 的核心概念

模型:fork、join 與結果點名

當指揮(也就是父任務)發出信號——子任務就各自去演奏自己的段落。這個時刻稱為 fork——就像把樂手分派到不同的廳房演奏各自的片段。

接著就會進入 join —— 指揮舉起指揮棒,大家回到台上,一起奏出最後的和弦。

然後你可以逐一詢問每位參與者,表演如何:

  • 透過 resultNow() 立即取得結果(若一切順利且無錯誤);
  • 透過 throwIfFailed() —— 確認沒有人走音。若有人真的亂了譜,會丟出統一的例外——就像指揮說:「我們的樂團出了狀況,重新來過」。

結束策略

每位指揮都有自己的停樂規則。在 Structured Concurrency 中,這由 結束策略 來決定:

  • ShutdownOnFailure —— 只要有任何樂手失了拍,指揮揮手:「停!重來」。其他人立刻停止演奏。
  • ShutdownOnSuccess —— 相反地,只要有人完美演完自己的段落,指揮就滿意地說:「夠了,不用再繼續,我們已經有贏家了」。其他人會安靜——也就是「第一個成功的答案」策略。

使用虛擬執行緒

每個子任務都由 StructuredTaskScope虛擬執行緒中啟動。這就像擁有一個懂事又迅速、不挑場地也不挑剔的樂團。你可以放心地建立數百、數千個這樣的執行者——它們不是笨重的系統執行緒,而是幾乎無重量的音符,只在需要時發聲。

3. 範例:HTTP 請求聚合器

來看一個實務情境:我們有三個資料來源(例如三個不同的伺服器),我們希望要嘛拿到全部的回應並加以聚合,要嘛取得第一個成功回應者的結果。

方案一:「全部都必須成功」(ShutdownOnFailure

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class AggregatorAllSuccess {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Future<String> f1 = scope.fork(() -> fetchFromSource1());
            Future<String> f2 = scope.fork(() -> fetchFromSource2());
            Future<String> f3 = scope.fork(() -> fetchFromSource3());

            scope.join(); // 等待所有任務完成
            scope.throwIfFailed(); // 只要有任何一個失敗,就拋出例外

            // 所有任務都成功——可以聚合結果
            String result = f1.resultNow() + f2.resultNow() + f3.resultNow();
            System.out.println("聚合結果: " + result);
        }
    }

    static String fetchFromSource1() { /* ... */ return "A"; }
    static String fetchFromSource2() { /* ... */ return "B"; }
    static String fetchFromSource3() { /* ... */ return "C"; }
}

發生了什麼:

  • 三個任務同時啟動(於虛擬執行緒中)。
  • 只要有任何一個失敗——其他任務就會被取消,並拋出例外。
  • 若全部成功——即可安全地聚合結果。

方案二:「第一個有效成功」(ShutdownOnSuccess

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class AggregatorFirstSuccess {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            Future<String> f1 = scope.fork(() -> fetchFromSource1());
            Future<String> f2 = scope.fork(() -> fetchFromSource2());
            Future<String> f3 = scope.fork(() -> fetchFromSource3());

            scope.join(); // 等待第一個成功
            scope.throwIfFailed(); // 若全部失敗——就拋出例外

            String result = scope.result(); // 第一個成功任務的結果
            System.out.println("第一個成功的結果: " + result);
        }
    }

    static String fetchFromSource1() { /* ... */ return "A"; }
    static String fetchFromSource2() { /* ... */ return "B"; }
    static String fetchFromSource3() { /* ... */ return "C"; }
}

發生了什麼:

  • 一旦有任務成功完成——其餘任務就會被取消。
  • 如果全部失敗——會拋出例外。

4. 自動取消與降級

StructuredTaskScope 會在策略需要時,自動負責取消其餘任務。舉例來說,若有任務失敗(ShutdownOnFailure),或有任務成功完成(ShutdownOnSuccess),其餘任務會收到取消信號(interrupt)。

範例:帶有逾時的妥善結束

import jdk.incubator.concurrent.StructuredTaskScope;
import java.time.Instant;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> fetchWithTimeout());
    Future<String> f2 = scope.fork(() -> fetchWithTimeout());

    scope.joinUntil(Instant.now().plusSeconds(2)); // 最多等待 2 秒
    scope.throwIfFailed();

    String result = f1.resultNow() + f2.resultNow();
    System.out.println(result);
}

若任務未在 2 秒內完成——會拋出例外,所有任務都會被取消。

5. 錯誤與例外處理

子任務拋出的例外如何被傳遞到 scope

有時演出中還是會有人彈錯音——StructuredTaskScope 不會裝作什麼都沒發生。它會仔細記錄是誰出了差錯,然後把完整報告交給指揮。當你呼叫 throwIfFailed() 時,它會丟出彙整過的例外——有點像總結報告:「以下是今天走音的名單」。必要時,你可以展開這棵「原因樹」,看看究竟是誰拖了後腿。而如果你只想知道某位演出者的情況——Future.exceptionNow() 會告訴你他的段落是如何收場的。

何時取消不等於失敗

務必記得:取消任務不代表錯誤。如果指揮宣布「演出結束」,樂手只是收好樂器——這是 cancelled,而不是 failed。只有真正演錯的情況才算錯誤,這種例外才會被彙入總結。

範例:原因樹

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> { throw new RuntimeException("錯誤 1"); });
    Future<String> f2 = scope.fork(() -> { throw new RuntimeException("錯誤 2"); });

    scope.join();
    scope.throwIfFailed(); // 會拋出包含兩個原因的例外
} catch (Exception e) {
    e.printStackTrace();
    // 可透過 e.getSuppressed() 取得 suppressed exceptions
}

6. 與 CompletableFuture 的比較

StructuredTaskScopeCompletableFuture 都能啟動平行任務,但:

  • StructuredTaskScope 適合任務在邏輯上相關、且應一起結束/取消的情況(任務階層)。
  • CompletableFuture 適合沒有階層關係的任務組合(例如轉換鏈、反應式情境)。

何時 StructuredTaskScope 能簡化程式碼:

  • 需要保證離開區塊前,所有子任務都已完成。
  • 需要集中式取消與錯誤處理。
  • 必須確保不會留下「懸掛」的任務。

何時 CompletableFuture 更方便:

  • 任務彼此無關,可以獨立存續。
  • 需要複雜的組合(thenCombinethenCompose 等)。

7. 實作:HTTP 請求聚合器

任務:向 3 個來源發送請求,取得第一個成功的回應

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class HttpAggregator {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
            Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));
            Future<String> f3 = scope.fork(() -> httpRequest("https://api3.example.com"));

            scope.join();
            scope.throwIfFailed();

            String result = scope.result();
            System.out.println("第一個成功的回應: " + result);
        }
    }

    static String httpRequest(String url) throws Exception {
        // 模擬請求(可使用 HttpClient)
        Thread.sleep((long) (Math.random() * 1000));
        if (Math.random() < 0.3) throw new RuntimeException("請求錯誤: " + url);
        return "來自 " + url + " 的回應";
    }
}

任務:若有任務失敗——正確地關停其餘任務

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
    Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));

    scope.join();
    scope.throwIfFailed();

    String result = f1.resultNow() + f2.resultNow();
    System.out.println("兩個回應: " + result);
} catch (Exception e) {
    System.err.println("其中一個任務出現錯誤: " + e.getMessage());
}

8. 使用 StructuredTaskScope 的常見錯誤

錯誤 #1:忘了呼叫 join()throwIfFailed()
若不呼叫 join(),任務可能在離開區塊前尚未完成。若不呼叫 throwIfFailed(),子任務的錯誤會被忽略。

錯誤 #2:在任務完成前嘗試取得結果。
在任務完成前呼叫 resultNow() 會丟出 IllegalStateException。請先透過 join() 等候完成。

錯誤 #3:忽略取消。
如果任務已被取消(例如因為 scope 的策略),請不要嘗試取得它的結果——否則會拋出例外。

錯誤 #4:混用不同的結束策略。
不應在 scope 內手動取消任務——請使用 ShutdownOnFailureShutdownOnSuccess 的策略。

錯誤 #5:在虛擬執行緒中執行耗時的 CPU-bound 任務。
StructuredTaskScope 預設使用虛擬執行緒——它們非常適合 I/O-bound 任務,但不會加速繁重的計算。

錯誤 #6:忘了關閉 scope(未使用 try-with-resources)。
StructuredTaskScope 實作了 AutoCloseable —— 請務必使用 try-with-resources,以保證所有任務都會被妥善結束。

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