Virtual Threads and Structured Concurrency
By: Daniel Hinojosa
Published on: October 8, 2025
Relationship Between Virtual Threads and Structured Concurrency
Overview
Virtual Threads and Structured Concurrency simplify concurrency in Java by improving readability, maintainability, and resource usage. While Virtual Threads focus on how tasks run, Structured Concurrency focuses on how tasks are scoped, coordinated, and cleaned up.
Virtual Threads: Lightweight Concurrency
Virtual Threads are JVM-managed, lightweight threads (Project Loom) that make “one-thread-per-task” practical.
-
Goal: Make massive concurrency cheap and straightforward.
-
Benefit: Reduces the need for complex async/reactive plumbing in many I/O-heavy apps.
-
Key insight: Decouples concurrency (number of tasks) from parallelism (number of CPUs).
try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> processOrder("order-123"));
}
Structured Concurrency: Organizing Concurrent Work
Structured Concurrency treats multiple concurrent subtasks as one logical unit. Tasks started in a scope are joined/cancelled together, preventing leaks and ensuring predictable lifetimes.
-
Goal: Bound task lifetimes to lexical scopes.
-
Benefit: Clearer error propagation, cooperative cancellation, and result aggregation.
try (var scope = new java.util.concurrent.StructuredTaskScope.ShutdownOnFailure()) {
var userTask = scope.fork(() -> findUserById(1L));
var ordersTask = scope.fork(() -> findOrdersForUserId(1L));
scope.join().throwIfFailed(); // waits and rethrows first failure
var user = userTask.get();
var orders = ordersTask.get();
return new UserOrders(user, orders);
}
As of JDK 25, Structured Concurrency has experienced some updates, especially in how shutdown on failure is handled. It is essential to note that Structured Concurrency is still in preview mode, as evidenced by the latest update at JEP 505. Therefore, how the new Structured Concurrency will look like the following:
try (var scope = StructuredTaskScope
.open(StructuredTaskScope.Joiner.allSuccessfulOrThrow())) {
var userTask = scope.fork(() -> userService.findUser(id));
var orderTask = scope.fork(() -> invoiceService.findAllInvoicesByUser(id));
scope.join();
var user = userTask.get();
var orders = ordersTask.get();
return new UserOrders(user, orders);
}
The key point to note above is how errors are handled with open
. open
now accepts an object called Joiner
. Joiner
has choices as to how you wish to handle the structured scope in case of failure:
-
allSuccessfulOrThrow()
- Returns a newJoiner
object that yields a stream of all subtasks when all subtasks complete successfully. -
allUntil
- Returns a newJoiner
object that yields a stream of all subtasks when all subtasks complete or a predicate returns{ true }
to cancel the scope. -
anySuccessfulResultOrThrow
- Returns a newJoiner
object that yields the result of any subtask that completed successfully. The Joiner causes join to throw if all subtasks fail -
awaitAll
- Returns a newJoiner
object that waits for all subtasks to complete. TheJoiner
does not cancel the scope if a subtask fails. -
awaitAllSuccessfulOrThrow
- Returns a newJoiner
object that waits for subtasks to complete successfully. TheJoiner
cancels the scope and causesjoin
to throw if any subtask fails.
How They Fit Together
-
Virtual Threads make it cheap to dedicate a thread per task.
-
Structured Concurrency makes it safe to manage many tasks as a single operation.
Together:
-
Each subtask runs in its own virtual thread.
-
The scope bounds lifetimes and handles cancellation/failures.
-
Errors propagate predictably; partial results don’t leak.
Tip
|
Virtual Threads remove the cost barrier to concurrency; Structured Concurrency removes the cognitive barrier. |
Analogy
Virtual Threads are the lightweight bricks. Structured Concurrency is the blueprint that ensures the building is stable and easy to maintain.
Summary Table
Concept | Focus | Key Benefit | Complements |
---|---|---|---|
Virtual Threads |
Execution model |
Cheap, scalable “thread-per-task” style; simpler blocking code |
Structured Concurrency |
Structured Concurrency |
Task organization & lifetime |
Bounded scopes, predictable failure/cancellation, result aggregation |
Virtual Threads |
Practical Guidance
-
Use Virtual Threads for I/O-heavy services, adapters, and blocking APIs.
-
Use Structured Concurrency when a request fans out into subtasks that must succeed/fail together.
-
Prefer both for request-per-thread servers, orchestration layers, and aggregator endpoints.
-
Still consider reactive/async only when you need streaming backpressure, cross-process pipelines, or specialized performance constraints.