CodeGym /Kurse /JAVA 25 SELF /Structured Concurrency

Structured Concurrency

JAVA 25 SELF
Level 58 , Lektion 0
Verfügbar

1. Einführung

Wenn Threads jeweils ihre eigene Partie spielen

Gewöhnliche Nebenläufigkeit erinnert oft an eine Probe ohne Dirigenten. Jeder Thread ist wie ein Musiker, der seine Melodie spielt, ohne auf die anderen zu hören. Manche sind früher fertig und gehen rauchen, manche hängen auf einem Akkord fest, andere verwechseln die Noten und produzieren einen Fehler. Das Ergebnis ist keine Symphonie, sondern eine Kakofonie: herauszufinden, wer wo aus dem Takt geraten ist, ist fast unmöglich, und alle auf einmal anzuhalten – ein echter Kraftakt.

Structured Concurrency löst dieses Problem. Aus disparaten Threads wird ein echtes Ensemble: Alle Aufgaben sind einem „Dirigenten“ unterstellt. Gibt er das Stoppsignal, verstummt das Orchester. Macht ein Musiker einen Fehler, halten die anderen geordnet an, ohne die Gesamtkomposition zu ruinieren. Alle Ergebnisse und Fehler werden zentral gesammelt, statt über den Code verstreut zu sein.

Stellen Sie sich vor: Sie schicken die Musiker nicht einfach los, irgendwas zu spielen, sondern versammeln sie in einem Saal. Es gibt einen Dirigenten, eine Partitur, und selbst wenn die Trompete falsch klingt, gerät das Orchester nicht aus der Fassung, sondern beendet den Auftritt sauber.

Was Structured Concurrency bietet

  • Ein einheitlicher „Scope“ pro Aufgabe: Alle Unteraufgaben leben innerhalb eines Codeblocks, ihr Lebenszyklus ist an diesen Block gebunden.
  • Vorhersagbares Beenden: Der übergeordnete Thread beendet sich nicht, bevor alle Unteraufgaben beendet sind.
  • Zentralisierte Stornierung: Wenn eine Aufgabe fehlschlägt oder der Parent beenden möchte, werden alle Unteraufgaben korrekt abgebrochen.
  • Konsistente Fehlerbehandlung: Fehler von Unteraufgaben werden aggregiert, man kann einen „Ursachenbaum“ (tree of causes) erhalten.
  • Sauberer, gut lesbarer Code: keine „hängenden“ Threads, keine vergessenen Aufgaben, kein Wettlauf um Abbrüche.

Structured Concurrency ist nicht nur ein neues API, sondern ein neues Denkmodell: Aufgaben sollten genauso strukturiert sein wie normale Codeblöcke (zum Beispiel try-with-resources).

2. Status von Structured Concurrency in Java

Zum Zeitpunkt der Erstellung dieses Kurses befindet sich Structured Concurrency im Status Preview (Java 21–23), aber es wird eine Weiterentwicklung zu GA (General Availability) in Java 24/25 erwartet. Das API liegt im Paket jdk.incubator.concurrent. Bevor Sie es im Produktiveinsatz verwenden, prüfen Sie unbedingt die aktuellen Release Notes Ihrer JDK-Version!

Wichtigste Klassen:

  • StructuredTaskScope – Basisklasse zur Verwaltung einer Aufgabengruppe.
  • Varianten: StructuredTaskScope.ShutdownOnFailure, StructuredTaskScope.ShutdownOnSuccess – Abschlussrichtlinien für Aufgaben.

Grundkonzepte von StructuredTaskScope

Modell: fork, join und freundliche Ergebnisabfrage

Wenn der Dirigent (also die übergeordnete Aufgabe) das Zeichen gibt, starten die Unteraufgaben ihre eigenen Parts. Dieser Moment heißt fork – als würden Sie die Musiker ihre Stücke in verschiedenen Sälen spielen lassen.

Dann kommt die Zeit für join – der Dirigent hebt den Taktstock, und alle kommen zusammen, um den finalen Akkord gemeinsam zu spielen.

Danach kann man bei jedem Teilnehmer nachfragen, wie alles gelaufen ist:

  • über resultNow() das Ergebnis sofort holen, wenn alles ohne Fehler gespielt wurde;
  • über throwIfFailed() sicherstellen, dass niemand falsch gespielt hat. Wenn doch jemand in den Noten durcheinanderkam, wird eine einheitliche Exception geworfen – als würde der Dirigent sagen: „Wir haben eine Störung im Orchester, wir fangen von vorn an“.

Abschlussrichtlinien

Jeder Dirigent hat seine eigene Regel, wann die Musik zu stoppen ist. In Structured Concurrency wird das durch eine Abschlussrichtlinie festgelegt:

  • ShutdownOnFailure – wenn auch nur ein Musiker aus dem Takt gerät, winkt der Dirigent ab: „Stopp! Noch einmal von vorne.“ Alle anderen hören sofort auf zu spielen.
  • ShutdownOnSuccess – umgekehrt: Sobald jemand seine Partie perfekt gespielt hat, ist der Dirigent zufrieden: „Genug, das reicht, wir haben schon einen Gewinner.“ Die anderen verstummen – Politik des ersten erfolgreichen Ergebnisses.

Arbeiten mit virtuellen Threads

Jede Unteraufgabe startet StructuredTaskScope in einem virtuellen Thread. Das ist, als hätten Sie ein Orchester, in dem jeder Musiker verständig und schnell ist, ohne Allüren und Bühnenanforderungen. Sie können bedenkenlos Hunderte, Tausende solcher Ausführenden erzeugen – das sind keine schwergewichtigen Threads, sondern nahezu gewichtlose Noten, die genau dann erklingen, wenn es nötig ist.

3. Beispiel: HTTP-Anfragen aggregieren

Sehen wir uns eine praktische Aufgabe an: Wir haben drei Datenquellen (z. B. drei unterschiedliche Server) und möchten entweder von allen eine Antwort bekommen (und diese aggregieren) oder die erste erfolgreiche Antwort verwenden.

Variante 1: „Alle müssen erfolgreich sein“ (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(); // warten, bis alle Aufgaben beendet sind
            scope.throwIfFailed(); // wenn eine fehlgeschlagen ist – Exception werfen

            // Alle Aufgaben erfolgreich – Ergebnisse können aggregiert werden
            String result = f1.resultNow() + f2.resultNow() + f3.resultNow();
            System.out.println("Aggregiertes Ergebnis: " + result);
        }
    }

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

Was passiert:

  • Alle drei Aufgaben starten parallel (in virtuellen Threads).
  • Wenn mindestens eine fehlschlägt, werden die übrigen abgebrochen und eine Exception wird geworfen.
  • Sind alle erfolgreich, lassen sich die Ergebnisse sicher aggregieren.

Variante 2: „Erfolg beim ersten gültigen Ergebnis“ (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(); // auf die erste erfolgreiche warten
            scope.throwIfFailed(); // wenn alle fehlgeschlagen sind – Exception werfen

            String result = scope.result(); // Ergebnis der ersten erfolgreichen Aufgabe
            System.out.println("Erstes erfolgreiches Ergebnis: " + result);
        }
    }

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

Was passiert:

  • Sobald eine Aufgabe erfolgreich beendet ist, werden die anderen abgebrochen.
  • Wenn alle fehlgeschlagen sind, wird eine Exception geworfen.

4. Automatischer Abbruch und Degradierung

StructuredTaskScope kümmert sich selbst um das Abbrechen der verbleibenden Aufgaben, wenn es die Richtlinie erfordert. Wenn zum Beispiel eine Aufgabe fehlschlägt (ShutdownOnFailure) oder eine erfolgreich beendet ist (ShutdownOnSuccess), erhalten die übrigen Aufgaben ein Abbruchsignal (interrupt).

Beispiel: sauberes Beenden mit Timeout

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)); // maximal 2 Sekunden warten
    scope.throwIfFailed();

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

Wenn die Aufgaben nicht innerhalb von 2 Sekunden beendet sind, wird eine Exception geworfen und alle Aufgaben werden abgebrochen.

5. Fehler und Ausnahmebehandlung

Wie Ausnahmen von Unteraufgaben in den Scope weitergeleitet werden

Manchmal verfehlt während des Konzerts doch jemand die richtigen Noten – StructuredTaskScope tut nicht so, als wäre nichts passiert. Es protokolliert sauber, wer daneben gespielt hat, und übergibt dem Dirigenten anschließend einen vollständigen Bericht. Wenn Sie throwIfFailed() aufrufen, wird eine aggregierte Exception geworfen – eine Art Sammelbericht: „Hier ist die Liste derer, die heute falsch gespielt haben.“ Bei Bedarf lässt sich dieser „Ursachenbaum“ aufklappen, um zu sehen, wer konkret versagt hat. Und wenn Sie Details zu einem bestimmten Ausführenden wissen möchten, sagt Ihnen Future.exceptionNow(), wie seine Partie ausgegangen ist.

Wenn Abbruch kein Fehlschlag ist

Wichtig: Das Abbrechen einer Aufgabe bedeutet nicht immer einen Fehler. Wenn der Dirigent sagt „Das Konzert ist zu Ende“, legen die Musiker einfach die Instrumente weg – das ist cancelled, aber nicht failed. Ein Fehler ist nur der Fall, wenn wirklich etwas Falsches gespielt wurde; diese Exception landet dann in der Gesamtschau.

Beispiel: Ursachenbaum

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

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

    scope.join();
    scope.throwIfFailed(); // wirft eine Exception mit beiden Ursachen
} catch (Exception e) {
    e.printStackTrace();
    // Unterdrückte Exceptions können über e.getSuppressed() abgerufen werden
}

6. Vergleich mit CompletableFuture

StructuredTaskScope und CompletableFuture ermöglichen beide das Starten paralleler Aufgaben, aber:

  • StructuredTaskScope ist dann praktisch, wenn Aufgaben logisch zusammengehören und gemeinsam beendet/abgebrochen werden sollen (Aufgabenhierarchie).
  • CompletableFuture ist gut für die Komposition ohne Hierarchie (z. B. Verarbeitungsketten, reaktive Szenarien).

Wann StructuredTaskScope den Code vereinfacht:

  • Wenn sichergestellt sein muss, dass alle Unteraufgaben vor dem Verlassen des Blocks beendet sind.
  • Wenn zentraler Abbruch und zentrale Fehlerbehandlung benötigt werden.
  • Wenn wichtig ist, dass keine „hängenden“ Aufgaben zurückbleiben.

Wann CompletableFuture geeigneter ist:

  • Wenn Aufgaben unabhängig sind und ihr eigenes Leben führen können.
  • Wenn eine komplexe Komposition benötigt wird (thenCombine, thenCompose usw.).

7. Praxis: Aggregator für HTTP-Anfragen

Aufgabe: An 3 Quellen anfragen, die erste erfolgreiche Antwort erhalten

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("Erste erfolgreiche Antwort: " + result);
        }
    }

    static String httpRequest(String url) throws Exception {
        // Anfrage simulieren (HttpClient kann verwendet werden)
        Thread.sleep((long) (Math.random() * 1000));
        if (Math.random() < 0.3) throw new RuntimeException("Fehler bei der Anfrage: " + url);
        return "Antwort von " + url;
    }
}

Aufgabe: Wenn eine Unteraufgabe fehlschlägt – die anderen sauber beenden

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("Beide Antworten: " + result);
} catch (Exception e) {
    System.err.println("Fehler in einer der Aufgaben: " + e.getMessage());
}

8. Typische Fehler bei der Arbeit mit StructuredTaskScope

Fehler Nr. 1: join() oder throwIfFailed() vergessen.
Wird join() nicht aufgerufen, könnten Aufgaben vor Verlassen des Blocks nicht beendet sein. Ohne throwIfFailed() bleiben Fehler von Unteraufgaben unbemerkt.

Fehler Nr. 2: Versuch, ein Ergebnis vor Abschluss der Aufgabe zu holen.
Ein Aufruf von resultNow() vor Abschluss der Aufgabe wirft IllegalStateException. Warten Sie zuerst per join() auf den Abschluss.

Fehler Nr. 3: Ignorieren des Abbruchs.
Wenn eine Aufgabe abgebrochen wurde (z. B. aufgrund der Scope-Richtlinie), versuchen Sie nicht, ihr Ergebnis zu holen – das führt zu einer Exception.

Fehler Nr. 4: Vermischen unterschiedlicher Abschlussrichtlinien.
Versuchen Sie nicht, Aufgaben innerhalb des Scope manuell abzubrechen – verwenden Sie die Richtlinien ShutdownOnFailure oder ShutdownOnSuccess.

Fehler Nr. 5: Lange CPU-bound-Aufgaben in virtuellen Threads starten.
StructuredTaskScope nutzt standardmäßig virtuelle Threads – ideal für I/O-bound-Aufgaben, aber sie beschleunigen schwere Berechnungen nicht.

Fehler Nr. 6: Scope nicht geschlossen (kein try-with-resources).
StructuredTaskScope implementiert AutoCloseable – verwenden Sie immer try-with-resources, um das Beenden aller Aufgaben zu garantieren.

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