Java 26: Structured Concurrency (JEP 525 — Preview)
Concurrent programming in Java just got a lot safer. JEP 525 continues Structured Concurrency as a preview API that treats groups of related concurrent tasks as a single unit of work with automatic lifecycle management.
Status: Sixth Preview — requires
--enable-preview
The Problem with ExecutorService
Traditional concurrent code has several issues:
- No automatic cancellation of other tasks when one fails
- No structured lifetime — threads can outlive the scope
- Hard to reason about error handling
- Thread dumps don’t show task relationships
The Solution: StructuredTaskScope
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> fetchUser());
var order = scope.fork(() -> fetchOrder());
scope.join(); // Wait for all
use(user.get(), order.get());
} // All tasks guaranteed complete hereWhen the scope closes, all tasks are guaranteed to be complete. If one fails, others can be automatically cancelled.
Key Joiners
awaitAll()— Wait for all tasks, regardless of outcomeawaitAllSuccessfulOrThrow()— Wait for all; throw on first failureallSuccessfulOrThrow()— Wait for all; return aListof resultsanySuccessfulOrThrow()— Return first success, cancel the restallUntil(Predicate)— Wait until predicate matches a result
Demo Highlights
1. Fork/Join All — Concurrent Data Fetching
Three simulated service calls run concurrently as virtual threads. Total time is the max of the individual calls, not the sum:
try (var scope = StructuredTaskScope.open()) {
var userTask = scope.fork(() -> fetchUser());
var orderTask = scope.fork(() -> fetchOrder());
var recsTask = scope.fork(() -> fetchRecommendations());
scope.join();
String user = userTask.get();
String order = orderTask.get();
String recs = recsTask.get();
// ~200ms total (slowest fork), not 150+200+100=450ms
}2. Race — First Successful Result Wins
anySuccessfulOrThrow() returns as soon as one task succeeds, automatically cancelling the remaining tasks:
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<String>anySuccessfulOrThrow())) {
scope.fork(() -> queryMirror("US-East", 200));
scope.fork(() -> queryMirror("EU-West", 100)); // Fastest!
scope.fork(() -> queryMirror("AP-South", 300));
String result = scope.join(); // Returns ~100ms after scope open
}3. Failure Handling — Automatic Cancellation
When one subtask throws an exception, awaitAllSuccessfulOrThrow() automatically cancels the remaining tasks and propagates the error:
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {
scope.fork(() -> { Thread.sleep(100); return "Task A done"; });
scope.fork(() -> { Thread.sleep(50); throw new RuntimeException("Task B failed!"); });
scope.fork(() -> { Thread.sleep(200); return "Task C done"; });
scope.join();
} catch (Exception e) {
// Task B failed → Tasks A and C automatically cancelled
System.out.println("Caught failure: " + e.getMessage());
}Real-World Use Cases
The companion StructuredConcurrencyRealWorldExamples class demonstrates six production scenarios:
API Aggregator (Backend-for-Frontend)
A mobile app’s BFF assembles a dashboard from 4 microservices concurrently. All tasks are bounded to the scope — if any critical service fails, the others are cancelled:
try (var scope = StructuredTaskScope.open()) {
var userTask = scope.fork(() -> fetchService("UserService", 80, "User{Alice, premium}"));
var orders = scope.fork(() -> fetchService("OrderService", 120, "Orders[3 pending]"));
var notifs = scope.fork(() -> fetchService("NotificationService", 60, "Notifs[5 unread]"));
var recs = scope.fork(() -> fetchService("RecommendationService", 100, "Recs[10 items]"));
scope.join();
// Sequential: ~360ms. Concurrent: ~120ms (slowest fork only)
var dashboard = new DashboardData(
userTask.get(), orders.get(), notifs.get(), recs.get());
}Price Comparison (Race Pattern)
Query multiple suppliers for the same product — return the fastest response, cancel the rest. Used by Google Flights, Kayak, and insurance quote aggregators:
record Quote(String supplier, double price, long latencyMs) {}
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<Quote>anySuccessfulOrThrow())) {
scope.fork(() -> { Thread.sleep(200); return new Quote("SupplierA", 29.99, 200); });
scope.fork(() -> { Thread.sleep(80); return new Quote("SupplierB", 31.99, 80); });
scope.fork(() -> { Thread.sleep(150); return new Quote("SupplierC", 27.99, 150); });
Quote fastest = scope.join();
// SupplierB wins in ~80ms; SupplierA and SupplierC are cancelled
}Health Check Dashboard (Fan-Out to All Services)
Unlike the race pattern, here every result matters — even failures. All health checks run in parallel:
try (var scope = StructuredTaskScope.open()) {
var auth = scope.fork(() -> checkHealth("auth-service", 50, true));
var payments = scope.fork(() -> checkHealth("payment-service", 80, true));
var catalog = scope.fork(() -> checkHealth("catalog-service", 120, false)); // down!
var search = scope.fork(() -> checkHealth("search-service", 40, true));
scope.join();
List<HealthResult> results = List.of(
auth.get(), payments.get(), catalog.get(), search.get());
long healthy = results.stream().filter(HealthResult::healthy).count();
System.out.println(healthy + "/" + results.size() + " services healthy");
}Payment with Fallback
Try the primary processor (Stripe). On failure, automatically retry with the fallback (PayPal). Structured concurrency ensures no zombie threads:
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {
scope.fork(() -> {
Thread.sleep(100);
throw new RuntimeException("Stripe gateway timeout");
});
scope.join();
return "Paid via Stripe";
} catch (Exception primaryFailure) {
// Fallback scope
try (var fallback = StructuredTaskScope.open()) {
var result = fallback.fork(() -> {
Thread.sleep(80);
return "PayPal txn-" + UUID.randomUUID().toString().substring(0, 8);
});
fallback.join();
return "✅ Paid via PayPal: " + result.get();
}
}Timeout-Bounded Fetch (Hard Deadline)
Enforce a deadline on a slow service. No leaked threads — structured concurrency guarantees cleanup:
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow(),
cf -> cf.withTimeout(Duration.ofMillis(200)))) {
var task = scope.fork(() -> {
Thread.sleep(serviceLatencyMs);
return "Service responded";
});
scope.join();
return "✅ " + task.get();
} catch (Exception e) {
return "⏱️ Timed out after 200ms → using fallback";
}Other Use Cases in the Demo
- Parallel search — search database, Elasticsearch, and external API concurrently; merge results
Running the Demo
mvn compile exec:exec \
-Dexec.mainClass=org.example.standard.StructuredConcurrencyDemoCheck out the full source on GitHub.