← Back to Advanced Java Course | Chapter 4: Modern Java Features | Lesson 10 of 12

Java Structured Concurrency

Structured Concurrency treats a group of related concurrent subtasks as a single unit with a shared lifetime, using StructuredTaskScope to fork, join, and propagate errors predictably.

What is Structured Concurrency?

Structured concurrency, previewed alongside virtual threads, treats a group of related concurrent subtasks as a single unit that starts and finishes together, replacing ad-hoc thread management with a scope that has a clear, predictable lifetime.

Example: What is Structured Concurrency?

java
public class Main {
	public static void main(String[] args) {
		System.out.println("Related subtasks start and finish together as one unit");
	}
}

StructuredTaskScope Basics

A StructuredTaskScope is opened in a try-with-resources block, and its fork method starts a subtask on its own virtual thread, while join blocks until every forked subtask in that scope has completed.

Example: StructuredTaskScope Basics

java
import java.util.concurrent.StructuredTaskScope;
public class Main {
	public static void main(String[] args) throws Exception {
		try (var scope = new StructuredTaskScope<String>()) {
			scope.fork(() -> "subtask result"); // runs on its own virtual thread
			scope.join(); // blocks until every forked subtask completes
			System.out.println("All subtasks finished");
		}
	}
}

Forking Subtasks

Multiple related subtasks, such as fetching different pieces of data needed for the same response, can be forked within one scope so they run concurrently, and each returns a Subtask handle used to retrieve its result later.

Example: Forking Subtasks

java
import java.util.concurrent.StructuredTaskScope;
public class Main {
	public static void main(String[] args) throws Exception {
		try (var scope = new StructuredTaskScope<Integer>()) {
			var task1 = scope.fork(() -> 10);
			var task2 = scope.fork(() -> 20); // both run concurrently
			scope.join();
			System.out.println(task1.get() + task2.get());
		}
	}
}

Joining and Handling Results

After join returns, every subtask's outcome is available: get() retrieves a successful result, and specialized scopes like ShutdownOnSuccess let the whole operation finish as soon as the first subtask succeeds, cancelling the rest.

Example: Joining and Handling Results

java
import java.util.concurrent.StructuredTaskScope;
public class Main {
	public static void main(String[] args) throws Exception {
		try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Integer>()) {
			scope.fork(() -> 1);
			scope.fork(() -> 2);
			scope.join();
			System.out.println("First success: " + scope.result()); // finishes on first success, cancels the rest
		}
	}
}

Error Propagation

ShutdownOnFailure cancels every other subtask in the scope as soon as any one of them fails, and throwIfFailed re-raises that failure back on the thread that owns the scope, giving structured, predictable error handling across concurrent subtasks.

Example: Error Propagation

java
import java.util.concurrent.StructuredTaskScope;
public class Main {
	public static void main(String[] args) throws Exception {
		try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
			scope.fork(() -> { throw new RuntimeException("subtask failed"); });
			scope.join();
			scope.throwIfFailed(); // re-raises the failure on the owning thread
		} catch (Exception e) {
			System.out.println("Caught: " + e.getCause().getMessage());
		}
	}
}

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.