Nebenläufigkeit in Java war lange von zwei Mustern geprägt: ExecutorService mit Future und CompletableFuture mit asynchronen Callbacks. Beide haben ihre Nachteile: Futures erfordern manuelle Synchronisation, und asynchrone Pipelines werden schnell unübersichtlich. Mit Java 21 (JEP 453) wurde Structured Concurrency als Preview-Feature eingeführt – ein Paradigma, das die Lebenszyklen nebenläufiger Tasks an den umgebenden Code knüpft und Task-Abbruch sowie Fehlerbehandlung fundamental vereinfacht.
Hinweis: Structured Concurrency ist zum Zeitpunkt der Artikelerstellung noch ein Preview-Feature und wurde in Java 22 (JEP 462), Java 23 (JEP 480) und Java 24 (JEP 499) weiter als Preview geführt. Die API kann sich bis zur Finalisierung noch ändern. Um die Beispiele auszuführen, muss mit --enable-preview kompiliert und gestartet werden.
Das Problem klassischer Nebenläufigkeit
Betrachten wir einen Service, der parallel drei externe APIs abfragt:
ExecutorService executor = Executors.newFixedThreadPool(3);
try {
Future<String> apiA = executor.submit(() -> rufeApiA());
Future<String> apiB = executor.submit(() -> rufeApiB());
Future<String> apiC = executor.submit(() -> rufeApiC());
String ergebnisA = apiA.get();
String ergebnisB = apiB.get();
String ergebnisC = apiC.get();
} finally {
executor.shutdown();
}
Code-Sprache: JavaScript (javascript)
Mehrere Probleme fallen auf:
- Wenn
apiA.get()eine Exception wirft, laufen die anderen Tasks weiter, ohne abgebrochen zu werden – Ressourcenverschwendung - Der
ExecutorServicemuss manuell heruntergefahren werden (sonst läuft er ewig) - Stacktraces sind schwer nachvollziehbar, weil Threads entkoppelt sind
StructuredTaskScope
StructuredTaskScope löst diese Probleme, indem es Tasks an einen definierten Scope bindet. Der Scope ist AutoCloseable – mit try-with-resources wird er automatisch geschlossen. Alle Tasks werden beim Verlassen des Scopes garantiert beendet.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> apiA = scope.fork(() -> rufeApiA());
Supplier<String> apiB = scope.fork(() -> rufeApiB());
Supplier<String> apiC = scope.fork(() -> rufeApiC());
scope.join(); <em>// Wartet auf alle Tasks und wirft bei Fehlern</em>
scope.throwIfFailed(); <em>// Propagiert Exceptions</em>
String ergebnis = apiA.get() + apiB.get() + apiC.get();
System.out.println(ergebnis);
}
Code-Sprache: JavaScript (javascript)
Das Verhalten ist radikal anders als bei ExecutorService:
- Schlägt ein einzelner Task fehl, werden alle anderen automatisch abgebrochen
- Der Scope wird durch try-with-resources automatisch geschlossen
- Der aufrufende Thread wird von
join()blockiert – keine versteckten Thread-Leaks mehr - Die Eltern-Kind-Beziehung der Threads bleibt erhalten – Stacktraces zeigen den vollständigen Aufrufbaum
ShutdownOnSuccess – Der schnellste gewinnt
Neben ShutdownOnFailure gibt es ShutdownOnSuccess, das sofort alle anderen Tasks abbricht, sobald ein Task erfolgreich war – ideal für redundante API-Abfragen:
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> rufeApiSpiegel1());
scope.fork(() -> rufeApiSpiegel2());
scope.fork(() -> rufeApiSpiegel3());
scope.join();
String ergebnis = scope.result();
System.out.println("Schnellste Antwort: " + ergebnis);
}
Code-Sprache: JavaScript (javascript)
Sobald die erste API antwortet, werden die beiden anderen Tasks abgebrochen – ohne manuelles cancel() oder Timeout-Logik.
Scoped Values als Ergänzung
Structured Concurrency wird oft mit Scoped Values kombiniert, um Kontext wie Request-IDs oder Benutzerinformationen ohne ThreadLocal weiterzureichen. Scoped Values wurden ebenfalls in Java 21 als Preview eingeführt (JEP 446) und sind bis einschließlich Java 24 weiterhin im Preview-Status:
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
ScopedValue.where(REQUEST_ID, "abc-123")
.run(() -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> verarbeiteMitRequestId(REQUEST_ID.get()));
scope.join();
}
});
Code-Sprache: PHP (php)
Jeder geforkte Task erbt den Scoped Value des Eltern-Threads automatisch – ohne explizites Kopieren.
Virtual Threads als Unterbau
Structured Concurrency entfaltet seine volle Kraft mit Virtual Threads – den leichtgewichtigen Threads, die Java 21 ebenfalls mitbrachte:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> rufeApiA());
scope.fork(() -> rufeApiB());
scope.fork(() -> rufeApiC());
scope.join();
}
}
Code-Sprache: JavaScript (javascript)
Der ExecutorService-Ersatz hier nutzt Virtual Threads: jeder scope.fork() erzeugt einen neuen Virtual Thread, der nur minimale Ressourcen verbraucht. Zehntausende parallele Aufgaben sind damit möglich – mit Platform Threads undenkbar.
Benutzerdefinierte Error-Handler
ShutdownOnFailure behandelt Fehler automatisch, aber für granulare Kontrolle lässt sich ein eigener StructuredTaskScope-Subtyp schreiben:
class BestEffortScope<T> extends StructuredTaskScope<T> {
private final List<T> results = new ArrayList<>();
@Override
protected void handleComplete(Subtask<? extends T> subtask) {
if (subtask.state() == Subtask.State.SUCCESS) {
results.add(subtask.get());
} else {
<em>// Fehler ignorieren, mit erfolgreichen Ergebnissen weitermachen</em>
log.warn("Task fehlgeschlagen: {}",
subtask.exception().getMessage());
}
}
@Override
public BestEffortScope<T> join() throws InterruptedException {
super.join();
return this;
}
public List<T> results() { return results; }
}
Code-Sprache: PHP (php)
Dieser „Best Effort“-Scope sammelt erfolgreiche Ergebnisse und ignoriert Fehler – nützlich, wenn mehrere optionale Datenquellen abgefragt werden.
Vergleich der Ansätze
| Kriterium | ExecutorService | StructuredTaskScope |
|---|---|---|
| Task-Abbruch bei Fehler | nein (manuell) | automatisch |
| Lebenszyklus | endlos (manuelles shutdown) | try-with-resources |
| Thread-Eltern-Beziehung | entkoppelt | erhalten |
| Stacktraces | fragmentiert | zusammenhängend |
| Virtual-Thread-kompatibel | ja | optimiert |
| Fehlerbehandlung | manuell per Future.get() | deklarativ per ShutdownOnFailure |
Fazit
Structured Concurrency bringt eine längst überfällige Ordnung in die nebenläufige Java-Programmierung. Tasks werden an definierte Scopes gebunden, automatisch beendet und Fehler werden konsistent propagiert. Mit Virtual Threads im Rücken und Scoped Values für die Kontext-Propagation entsteht ein Programmiermodell, das die Komplexität klassischer Threads und Futures radikal reduziert. Wer heute neue Microservices in Java 21+ schreibt, sollte StructuredTaskScope dem ExecutorService vorziehen – der Code wird kürzer, lesbarer und korrekter. Allerdings sollte beachtet werden, dass die API noch im Preview-Status ist und sich bis zur Finalisierung noch ändern kann.