Back to Blog
java-23structured-concurrencyconcurrencyperformance

Java 23: Structured Concurrency Finalized - Goodbye Thread Spaghetti

Java 23 finalizes Structured Concurrency. Parent-child task relationships. Automatic cancellation. Clean concurrent code without complexity.

J

JOptimize Team

August 26, 2026· 12 min read

Java 23 finalizes Structured Concurrency.

No more thread spaghetti. No more tasks running after parent dies. No more cancellation forgotten.

Parent spawns children. Children finish before parent. Automatic timeout. Automatic cancellation.

The Problem It Solves

Current Java concurrency (ugly):

public void process() throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); Future<Result> task1 = executor.submit(() -> work1()); Future<Result> task2 = executor.submit(() -> work2()); Future<Result> task3 = executor.submit(() -> work3()); try { Result r1 = task1.get(5, TimeUnit.SECONDS); Result r2 = task2.get(5, TimeUnit.SECONDS); Result r3 = task3.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { // Hope tasks cancel? Nope, they keep running task1.cancel(true); task2.cancel(true); task3.cancel(true); } finally { executor.shutdown(); // May not actually shutdown executor.awaitTermination(1, TimeUnit.SECONDS); } }

Tasks may still be running after method exits. Cancellation unreliable. Timeout management complex.

Java 23 Structured Concurrency

public void process() throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // Parent spawns children Subtask<Result> task1 = scope.fork(() -> work1()); Subtask<Result> task2 = scope.fork(() -> work2()); Subtask<Result> task3 = scope.fork(() -> work3()); // Wait for all children with timeout scope.joinUntil(Instant.now().plus(Duration.ofSeconds(5))); // If here: all children completed successfully Result r1 = task1.get(); Result r2 = task2.get(); Result r3 = task3.get(); } // On exit: all children cancelled automatically }

Clean. Children guaranteed finished before parent exits. Automatic cancellation.

Structured Relationships

Parent Task
    ├─ Child 1 (work1)
    ├─ Child 2 (work2)
    └─ Child 3 (work3)

Parent waits for all children.
Parent exits → children cancelled.
Child exception → siblings cancelled.

No orphaned tasks.

Three Policies

ShutdownOnFailure

One failure = cancel all siblings:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var task1 = scope.fork(() -> apiCall1()); var task2 = scope.fork(() -> apiCall2()); var task3 = scope.fork(() -> apiCall3()); scope.joinUntil(deadline); // If any task failed, others cancelled }

Useful for all-or-nothing operations.

ShutdownOnSuccess

First success = cancel others:

try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Integer>()) { // Try 3 mirrors of same data var t1 = scope.fork(() -> fetchFromServer1()); var t2 = scope.fork(() -> fetchFromServer2()); var t3 = scope.fork(() -> fetchFromServer3()); scope.joinUntil(deadline); // First success wins, others cancelled int result = scope.result(); }

Useful for failover and racing.

Custom Policy

Define custom cancellation logic:

class MajorityPolicy<T> extends StructuredTaskScope<T> { @Override protected void handleComplete(Subtask<? extends T> subtask) { if (subtask.state() == Subtask.State.SUCCESS) { successCount++; } if (successCount > total / 2) { shutdown(); // Majority reached } } }

Real Case: Multi-Source Data Load

Old (messy):

public UserData loadUser(String id) throws Exception { ExecutorService exec = Executors.newFixedThreadPool(3); Future<UserProfile> profileFuture = exec.submit(() -> apiClient.getProfile(id)); Future<List<Order>> ordersFuture = exec.submit(() -> apiClient.getOrders(id)); Future<Preferences> prefsFuture = exec.submit(() -> apiClient.getPreferences(id)); try { UserProfile profile = profileFuture.get(2, TimeUnit.SECONDS); List<Order> orders = ordersFuture.get(2, TimeUnit.SECONDS); Preferences prefs = prefsFuture.get(2, TimeUnit.SECONDS); return new UserData(profile, orders, prefs); } catch (TimeoutException e) { profileFuture.cancel(true); ordersFuture.cancel(true); prefsFuture.cancel(true); throw new Exception("Timeout loading user data"); } finally { exec.shutdown(); } }

New (clean):

public UserData loadUser(String id) throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var profile = scope.fork(() -> apiClient.getProfile(id)); var orders = scope.fork(() -> apiClient.getOrders(id)); var prefs = scope.fork(() -> apiClient.getPreferences(id)); scope.joinUntil(Instant.now().plus(Duration.ofSeconds(2))); return new UserData(profile.get(), orders.get(), prefs.get()); } }

4 lines vs 30 lines. Automatic cancellation. No resource leaks.

Performance Impact

  • Virtual threads + structured concurrency = 100-1000x better throughput
  • Automatic cancellation reduces resource waste
  • Less GC from task management
  • Predictable memory usage

Migration Path

Java 23: Production-ready.

Adopt gradually:

  1. New code uses structured concurrency
  2. Refactor ExecutorService patterns over time
  3. Spring framework support (6.2+)
  4. Quarkus, Micronaut already have support

Production Checklist

  1. Upgrade to Java 23+
  2. Replace ExecutorService with StructuredTaskScope
  3. Choose policy: ShutdownOnFailure or ShutdownOnSuccess
  4. Use try-with-resources for scope
  5. Set joinUntil deadline
  6. Handle task exceptions explicitly
  7. Test cancellation under timeout
  8. Monitor task completion rates
  9. Combine with virtual threads for max throughput
  10. Document cancellation expectations

Summary

Structured Concurrency finalizes in Java 23.

Parent-child relationships explicit. Automatic cancellation. Clean code.

No more thread management nightmares.

Optimize with JOptimize PRO. Use code LINKEDIN40 for 40% OFF.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.