Virtual Threads and Structured Concurrency

By: Daniel Hinojosa

Published on: October 8, 2025

Categories: Java Virtual Threads Structured Concurrency Loom

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:

  1. allSuccessfulOrThrow() - Returns a new Joiner object that yields a stream of all subtasks when all subtasks complete successfully.

  2. allUntil - Returns a new Joiner object that yields a stream of all subtasks when all subtasks complete or a predicate returns { true } to cancel the scope.

  3. anySuccessfulResultOrThrow - Returns a new Joiner object that yields the result of any subtask that completed successfully. The Joiner causes join to throw if all subtasks fail

  4. awaitAll - Returns a new Joiner object that waits for all subtasks to complete. The Joiner does not cancel the scope if a subtask fails.

  5. awaitAllSuccessfulOrThrow - Returns a new Joiner object that waits for subtasks to complete successfully. The Joiner cancels the scope and causes join 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.