Prev Next

Java / Java 25 Structured Concurrency Interview questions

Last updated

1. What is structured concurrency in Java? 2. What problem does structured concurrency solve? 3. What is StructuredTaskScope? 4. Is structured concurrency final in Java 25? 5. How do you enable the structured concurrency preview in Java 25? 6. What is a Subtask in StructuredTaskScope? 7. How do you fork a subtask in a scope? 8. What does scope.join() do? 9. What is a Joiner in StructuredTaskScope? 10. List the built-in Joiner factory methods in Java 25? 11. How do you open a StructuredTaskScope in Java 25? 12. What are the states of a Subtask? 13. What is the default Joiner when you call open() with no arguments? 14. What exceptions can StructuredTaskScope.join() throw? 15. Why is structured concurrency still a preview API in Java 25? 16. How do you read a subtask's result? 17. Why is StructuredTaskScope used with try-with-resources? 18. What type of thread does a subtask run on by default? 19. What is StructuredTaskScope.FailedException? 20. What is the owner thread of a scope? 21. What is the difference between StructuredTaskScope and ExecutorService? 22. How does allSuccessfulOrThrow() handle a failing subtask? 23. How does anySuccessfulOrThrow() work? 24. How does awaitAll() differ from awaitAllSuccessfulOrThrow()? 25. How do you set a timeout on a StructuredTaskScope? 26. How does cancellation work in a StructuredTaskScope? 27. How do you set a scope name and thread factory? 28. Why can only the owner thread call join() and close()? 29. What happens if you forget to call join()? 30. What is StructureViolationException? 31. How do scoped values work with structured concurrency? 32. Why are ThreadLocals discouraged with subtasks? 33. How do nested scopes behave in structured concurrency? 34. What is allUntil() and when should you use it? 35. What is the difference between a Joiner and the old ShutdownOnFailure? 36. How do you write a custom Joiner? 37. What happens when one subtask throws while others are still running? 38. What happens if the owner thread is interrupted during join()? 39. How do you return partial results when one subtask is slow? 40. Why should subtask code handle interruption correctly? 41. How do you troubleshoot structured concurrency with thread dumps? 42. How does structured concurrency compare with CompletableFuture? 43. How do you test code that uses StructuredTaskScope? 44. How can you limit concurrency inside a StructuredTaskScope? 45. Why does synchronized pinning matter for subtasks? 46. How do you unwrap FailedException to handle the real error? 47. When would you choose structured concurrency over parallel streams? 48. Explain the lifecycle of a StructuredTaskScope? 49. Explain the execution flow of a fan-out request using a scope? 50. How do you migrate from the Java 21-24 StructuredTaskScope API to Java 25?

1. What is structured concurrency in Java?

Structured concurrency treats a group of related tasks running in different threads as one unit of work. The code that starts the tasks is the same code that waits for them, so their lifetime is confined to a single block.

In Java 25 it is exposed through StructuredTaskScope, a preview API. You open a scope in a try-with-resources block, fork subtasks, join, and read results. When the block exits, no subtask is still running.

try (var scope = StructuredTaskScope.open()) {
    var user  = scope.fork(() -> findUser(id));
    var order = scope.fork(() -> findOrder(id));
    scope.join();
    return new Page(user.get(), order.get());
}

Take quiz
In structured concurrency, when does a subtask's lifetime end?
When the JVM shuts down
When its thread is garbage collected
Before the block that forked it exits
Whenever the thread pool decides
Which class is the main entry point for structured concurrency in Java 25?
ThreadGroup
ExecutorCompletionService
ForkJoinPool.commonPool()
StructuredTaskScope

2. What problem does structured concurrency solve?

It fixes the gap between how code looks and how it runs. With plain ExecutorService and Future, a method can return while tasks it started are still running somewhere else.

  • Thread leaks: if one future fails, its siblings keep running unless you cancel them by hand.
  • Lost errors: a failure in a worker thread is only seen if someone calls get().
  • Weak observability: thread dumps show no link between a task and the request that started it.

Structured concurrency ties subtasks to a scope so failure, cancellation and cleanup follow the code's block structure.

Take quiz
Two Futures run on an ExecutorService and one fails fast. What happens to the other by default?
It is cancelled automatically
It is restarted on another thread
It throws the same exception
It keeps running unless you cancel it manually
Which of these is NOT a problem structured concurrency targets?
Slow SQL query plans
Orphaned subtasks
Missing cancellation of siblings
Unclear task-to-request relationships in thread dumps

3. What is StructuredTaskScope?

StructuredTaskScope<T, R> is the class in java.util.concurrent that owns a set of subtasks and enforces their structure. It implements AutoCloseable, so it is always used in try-with-resources.

The type parameters matter: T is the result type of the subtasks, and R is what join() returns. A scope is created with the static open() factory methods, not with a constructor.

In Java 25 it is a preview API (JEP 505), so you need --enable-preview to compile and run code that uses it.

Take quiz
In StructuredTaskScope<T, R>, what does R represent?
The result returned by join()
The thread type used for subtasks
The request type of the caller
The number of subtasks allowed
How do you create a scope in Java 25?
new StructuredTaskScope()
StructuredTaskScope.open()
StructuredTaskScope.start()
Executors.newStructuredScope()

4. Is structured concurrency final in Java 25?

No. Java 25 (an LTS release) ships structured concurrency as its fifth preview, defined by JEP 505. Preview means the API is complete enough to use but can still change in a later release.

API Status in Java 25
StructuredTaskScope Preview (JEP 505)
ScopedValue Final (JEP 506)
Virtual threads Final (since Java 21)

So you can try it in real code, but expect to pass --enable-preview and to revisit usage when the API settles.

Take quiz
Which JEP covers structured concurrency in Java 25?
JEP 506
JEP 505
JEP 491
JEP 453
Which related API became final in Java 25?
StructuredTaskScope
StructuredTaskScope.Joiner
ScopedValue
StructureViolationException

5. How do you enable the structured concurrency preview in Java 25?

Pass --enable-preview at both compile time and run time, and compile against release 25.

javac --release 25 --enable-preview App.java
java  --enable-preview App

# single-file source launcher
java --enable-preview --source 25 App.java

In Maven, add <compilerArgs><arg>--enable-preview</arg></compilerArgs> with <release>25</release>, and set the same flag in Surefire's argLine. In Gradle, add it to compilerArgs and test jvmArgs.

Classes compiled with preview features are marked, and they only run on the same JDK feature release.

Take quiz
Which javac options compile preview code for Java 25?
--preview-api 25
-XX:+EnablePreview
--release 25 --enable-preview
--add-preview java.base
Why won't a class compiled with preview features run on Java 26?
Preview classes are encrypted
Java 26 deletes StructuredTaskScope
Virtual threads are disabled in 26
Its class file is tied to the exact JDK feature release

6. What is a Subtask in StructuredTaskScope?

StructuredTaskScope.Subtask<T> is the handle returned by fork(). It represents one forked task and extends Supplier<T>.

  • get(): the result, valid only after a successful join().
  • exception(): the failure, only when the state is FAILED.
  • state(): UNAVAILABLE, SUCCESS or FAILED.
  • task(): the Callable or Runnable that was forked.

Unlike Future, it has no blocking get(timeout) and no cancel(). Waiting and cancelling belong to the scope.

Take quiz
What does fork() return?
A Future
A CompletableFuture
A Thread
A Subtask
Which functional interface does Subtask extend?
Supplier
Callable
Runnable
Function

7. How do you fork a subtask in a scope?

Call scope.fork(...) with a Callable or a Runnable. It starts the task in a new thread and returns a Subtask straight away, without waiting for the task to finish.

try (var scope = StructuredTaskScope.open()) {
    Subtask<String> name  = scope.fork(() -> loadName());
    Subtask<Integer> age  = scope.fork(() -> loadAge());
    scope.join();                       // wait before reading
    System.out.println(name.get() + " " + age.get());
}

Always call join() before get(). Reading a result earlier throws IllegalStateException.

Take quiz
Does fork() block until the subtask completes?
No, it returns immediately
Yes, always
Only for Runnable tasks
Only when a timeout is configured
Which overloads of fork exist?
fork(Supplier) only
fork(Callable) and fork(Runnable)
fork(Thread)
fork(Future)

8. What does scope.join() do?

join() blocks the owner thread until every subtask is done, or until the joiner decides the scope should be cancelled (for example, after the first failure). It then returns whatever the joiner produces.

It can throw InterruptedException if the owner is interrupted, StructuredTaskScope.FailedException if the joiner reports a failure, and a timeout exception if a configured timeout expires.

Only the owner thread may call it, and it must be called after forking and before reading results or closing the scope.

Take quiz
What must happen before Subtask.get() is safe to call?
close() has been called
join() has returned normally
The owner thread has slept
A second subtask has been forked
Which thread may call join()?
Any platform thread
Any virtual thread
Only the scope's owner thread
Only the last forked subtask

9. What is a Joiner in StructuredTaskScope?

A Joiner<T, R> is the policy object you pass to open(). It decides when the scope is finished, what to do with each subtask result, and what join() returns.

It has three methods: onFork(subtask) and onComplete(subtask), both returning a boolean, and result(). If either boolean method returns true, the scope is cancelled and remaining subtasks are interrupted.

Joiners replaced the old subclass approach (ShutdownOnFailure, ShutdownOnSuccess) with a composable, testable interface.

Take quiz
Which Joiner method provides the value returned by join()?
onFork()
onComplete()
result()
close()
If onComplete returns true, what happens to the scope?
It is restarted
It pauses until the owner resumes it
It closes and rethrows the subtask exception
It is cancelled

10. List the built-in Joiner factory methods in Java 25?

Factory method join() returns Behaviour
awaitAllSuccessfulOrThrow() Void Waits for all; cancels on first failure (default)
allSuccessfulOrThrow() List<T> Collects all results; cancels on first failure
anySuccessfulOrThrow() T First success wins; others cancelled
awaitAll() Void Waits for all; failures do not cancel
allUntil(Predicate) List<Subtask<T>> Cancels when the predicate returns true

For anything else, implement the Joiner interface yourself.

Take quiz
Which Joiner returns a List<T> of subtask results?
awaitAll()
anySuccessfulOrThrow()
awaitAllSuccessfulOrThrow()
allSuccessfulOrThrow()
Which Joiner suits racing several sources and taking the fastest?
anySuccessfulOrThrow()
awaitAll()
allSuccessfulOrThrow()
awaitAllSuccessfulOrThrow()

11. How do you open a StructuredTaskScope in Java 25?

Use one of the static open() overloads. Constructors are no longer part of the API.

// default joiner: awaitAllSuccessfulOrThrow()
var s1 = StructuredTaskScope.open();

// choose a joiner
var s2 = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow());

// joiner + configuration
var s3 = StructuredTaskScope.open(
        Joiner.<String>anySuccessfulOrThrow(),
        cf -> cf.withName("quote-race").withTimeout(Duration.ofSeconds(2)));

Always assign it in a try-with-resources header so close() runs on every path.

Take quiz
Which call opens a scope that collects every result into a list?
StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())
new StructuredTaskScope.ShutdownOnFailure()
StructuredTaskScope.of(List.class)
Joiner.open(scope)
Where is a scope timeout configured?
As an argument to fork()
In the Configuration function passed to open()
In a system property
Through Joiner.timeout()

12. What are the states of a Subtask?

State Meaning Safe call
UNAVAILABLE Not finished, or cancelled before it could complete none
SUCCESS Completed with a result get()
FAILED Completed with an exception exception()

Check state() after join() when you use a joiner such as awaitAll(), which does not stop on failure. Calling get() on a non-SUCCESS subtask throws IllegalStateException.

Take quiz
Which state describes a subtask that was cancelled before completing?
PENDING
UNAVAILABLE
CANCELLED
RUNNING
Which state makes exception() safe to call?
SUCCESS
UNAVAILABLE
FAILED
Any state

13. What is the default Joiner when you call open() with no arguments?

It is Joiner.awaitAllSuccessfulOrThrow(). The scope waits for all subtasks. If one fails, the scope is cancelled and join() throws FailedException with that failure as the cause.

The joiner produces no value, so join() returns null (type Void). You read each result through its own Subtask handle.

try (var scope = StructuredTaskScope.open()) {
    var a = scope.fork(() -> 1);
    var b = scope.fork(() -> 2);
    scope.join();                 // throws FailedException on any failure
    int sum = a.get() + b.get();
}

Take quiz
What does join() return for a scope opened with no joiner?
A list of results
The first result
null (Void)
The owner thread
What does the default joiner do when a subtask fails?
Ignores the failure
Waits for the rest, then returns normally
Retries the subtask once
Cancels the scope and join() throws FailedException

14. What exceptions can StructuredTaskScope.join() throw?

Exception When
InterruptedException Owner thread interrupted while waiting
StructuredTaskScope.FailedException Joiner's result() reports a subtask failure
StructuredTaskScope.TimeoutException Configured timeout expired before completion
WrongThreadException Caller is not the owner thread
IllegalStateException Scope already closed or used out of order

Only InterruptedException is checked. The others are unchecked, so you decide where to handle them.

Take quiz
Which exception wraps a subtask failure thrown from join()?
ExecutionException
CompletionException
RejectedExecutionException
StructuredTaskScope.FailedException
Which checked exception can join() throw?
InterruptedException
TimeoutException from java.util.concurrent
FailedException
WrongThreadException

15. Why is structured concurrency still a preview API in Java 25?

The API has changed in almost every round, and the maintainers want more real-world feedback before locking it in. The path was incubator in Java 19 and 20, then previews in 21, 22, 23, 24 and 25.

Java 25's JEP 505 made the biggest change so far. The public constructors and the ShutdownOnFailure and ShutdownOnSuccess subclasses were replaced by static open() methods plus Joiner policies. join() now returns a value, and the deadline-based joinUntil became a timeout configuration.

Marking it preview lets the JDK team keep reshaping the API without breaking a stable contract.

Take quiz
What was the main API change in the Java 25 preview?
Static open() methods with Joiner policies replaced constructors and subclasses
fork() became blocking
Subtask was removed
ThreadGroup support was added
In which release did structured concurrency first appear as a preview?
Java 17
Java 21
Java 22
Java 25

16. How do you read a subtask's result?

Call get() on the Subtask after join() has returned. For a subtask that failed, use exception() instead.

scope.join();
if (task.state() == Subtask.State.SUCCESS) {
    use(task.get());
} else if (task.state() == Subtask.State.FAILED) {
    log.warn("failed", task.exception());
}

Calling get() before join(), or on a subtask that is not SUCCESS, throws IllegalStateException. With allSuccessfulOrThrow() you can skip handles entirely and use the list that join() returns.

Take quiz
What does get() throw when called on a FAILED subtask?
ExecutionException
IllegalStateException
The original exception
Nothing, it returns null
With allSuccessfulOrThrow(), what is the cleanest way to get results?
Call scope.get()
Read thread locals
Use the List returned by join()
Fork the tasks again

17. Why is StructuredTaskScope used with try-with-resources?

Because close() is what enforces the structure. When the block ends, close() cancels any subtask still running by interrupting it, and then waits for every subtask thread to finish.

That guarantee is why no subtask can outlive the block. If you forked tasks but never called join(), close() still cleans up, but it throws IllegalStateException to flag the bug.

Only the owner thread may close the scope. Leaving it open, as you could with an executor, defeats the model.

Take quiz
What does close() do with subtasks that are still running?
Lets them continue in the background
Stops them with Thread.stop()
Interrupts them and waits for their threads to end
Ignores them
Why is a try-with-resources header preferred for a scope?
It makes fork() faster
It enables the preview feature
It selects the thread factory
It guarantees the scope is closed on every code path

18. What type of thread does a subtask run on by default?

Each subtask runs in its own new virtual thread. Virtual threads are cheap, so one thread per task is fine even for thousands of subtasks, and there is no pool to size or share.

You can change this with Configuration.withThreadFactory(...), for example to name threads or to use platform threads for a special case.

var factory = Thread.ofVirtual().name("fetch-", 0).factory();
try (var scope = StructuredTaskScope.open(
        Joiner.<String>allSuccessfulOrThrow(),
        cf -> cf.withThreadFactory(factory))) {
    // ...
}

Take quiz
How many threads does a default scope use per subtask?
A shared platform pool
The common ForkJoinPool
The owner thread, reused
One new virtual thread each
How do you replace the default thread type?
cf.withThreadFactory(factory)
System.setProperty("scope.threads", ...)
fork(thread)
Subclass Subtask

19. What is StructuredTaskScope.FailedException?

It is the unchecked exception join() throws when a failure-aware joiner such as allSuccessfulOrThrow() or the default joiner sees a subtask fail. It extends RuntimeException.

The real error is available through getCause(). This keeps one predictable type at the join point while the original exception is preserved.

try {
    scope.join();
} catch (StructuredTaskScope.FailedException e) {
    Throwable root = e.getCause();   // exception from the failed subtask
}

Take quiz
What kind of exception is FailedException?
Unchecked, a RuntimeException subclass
Checked, an Exception subclass
An Error subclass
Thrown by fork()
Where do you find the subtask's original exception?
scope.exception()
e.getCause()
Thread.getUncaughtExceptionHandler()
e.getMessage() only

20. What is the owner thread of a scope?

The owner is the thread that called open(). It controls the scope's lifecycle, so only it may call join() and close(). Another thread doing so gets WrongThreadException.

Subtasks do not own the scope. They can do work and, if they are inside the scope's thread tree, fork into it, but they cannot decide when it ends.

This single-owner rule keeps lifetime tied to one block of code.

Take quiz
Which exception is thrown if a non-owner thread calls join()?
IllegalMonitorStateException
WrongThreadException
SecurityException
StructureViolationException
Who is the owner of a scope?
The first forked subtask
Always the main thread
The thread that opened it
The thread with highest priority

21. What is the difference between StructuredTaskScope and ExecutorService?

Aspect ExecutorService StructuredTaskScope
Task lifetime Independent of caller Bound to the try block
Failure handling Manual via Future.get() Joiner policy, cancels siblings
Cancellation Manual future.cancel() Automatic on failure, success or timeout
Threads Usually a shared pool New virtual thread per subtask
Lifetime of the object Long-lived, shared Short-lived, per operation

Use an executor for long-lived shared workers. Use a scope for a fan-out where all subtasks serve one operation.

Take quiz
Who cancels sibling tasks after a failure in a StructuredTaskScope?
You, with future.cancel()
The operating system
The scope and joiner, automatically
The garbage collector
A long-lived shared worker pool fits better with:
StructuredTaskScope, since it pools threads
Neither
Only ScopedValue
ExecutorService

22. How does allSuccessfulOrThrow() handle a failing subtask?

As each subtask completes, the joiner stores successful results. On the first failure it records the exception and returns true from onComplete, which cancels the scope and interrupts the other subtasks.

join() then throws FailedException with the first failure as its cause. If everything succeeds, join() returns a List<T> of results in the order the subtasks were forked.

try (var scope = StructuredTaskScope.open(Joiner.<Price>allSuccessfulOrThrow())) {
    scope.fork(() -> priceFrom("A"));
    scope.fork(() -> priceFrom("B"));
    List<Price> prices = scope.join();
}

Take quiz
What does allSuccessfulOrThrow() do on the first failed subtask?
Waits for all, then returns partial results
Retries the failed subtask
Returns an empty list
Cancels the scope and join() throws FailedException
What does join() return when all subtasks succeed with allSuccessfulOrThrow()?
A List of results
A single result
null
A Stream of Subtask objects

23. How does anySuccessfulOrThrow() work?

It returns the result of the first subtask that succeeds and cancels the rest. Failures are ignored as long as at least one subtask succeeds.

If every subtask fails, join() throws FailedException carrying a subtask failure as its cause. It fits redundant lookups, like asking three mirrors for the same quote and using the fastest answer.

try (var scope = StructuredTaskScope.open(Joiner.<Quote>anySuccessfulOrThrow())) {
    scope.fork(() -> mirrorA.quote(sym));
    scope.fork(() -> mirrorB.quote(sym));
    Quote fastest = scope.join();
}

Take quiz
What is a typical use of anySuccessfulOrThrow()?
Querying redundant replicas and taking the fastest answer
Summing independent numbers
Waiting for every task
Retrying the same task
What happens if every subtask fails with anySuccessfulOrThrow()?
join() returns null
join() throws FailedException
join() blocks forever
join() returns the last result

24. How does awaitAll() differ from awaitAllSuccessfulOrThrow()?

awaitAll() awaitAllSuccessfulOrThrow()
On failure Keeps waiting for others Cancels scope, join() throws
join() result null null
Who checks errors You, through state() The joiner
Best for Collecting partial results All-or-nothing work

With awaitAll(), every subtask runs to the end and you decide what a failure means. Remember to inspect each subtask's state before calling get().

Take quiz
Which joiner lets all subtasks finish even when one fails?
awaitAllSuccessfulOrThrow()
awaitAll()
anySuccessfulOrThrow()
allSuccessfulOrThrow()
With awaitAll(), how do you find out a subtask failed?
join() throws FailedException
Catch ExecutionException
Check subtask.state() after join()
Read the scope's error log

25. How do you set a timeout on a StructuredTaskScope?

Use the configuration function passed to open(): cf.withTimeout(Duration). The clock starts when the scope opens.

try (var scope = StructuredTaskScope.open(
        Joiner.<String>allSuccessfulOrThrow(),
        cf -> cf.withTimeout(Duration.ofSeconds(3)))) {
    scope.fork(() -> slowCall());
    return scope.join();
} catch (StructuredTaskScope.TimeoutException e) {
    return fallback();
}

When the time runs out, the scope is cancelled, running subtasks are interrupted, and join() throws a timeout exception. This replaces the older joinUntil(Instant).

Take quiz
Which exception does join() throw when the scope timeout expires?
InterruptedException
SocketTimeoutException
StructuredTaskScope.TimeoutException
DeadlineExceededError
What happens to running subtasks when the timeout expires?
They keep running
They are paused
They finish and results are silently dropped
They are cancelled by interruption

26. How does cancellation work in a StructuredTaskScope?

Cancellation is cooperative and interrupt-based. A scope is cancelled when the joiner returns true from onFork or onComplete, when the timeout expires, or when the owner is interrupted during join().

  1. The scope marks itself cancelled.
  2. Every running subtask thread is interrupted.
  3. Forks made after cancellation are not run; their subtasks stay UNAVAILABLE.
  4. close() waits for all threads to exit.

Blocking calls like sleep, I/O on virtual threads and queue operations respond to interrupts. A tight CPU loop does not unless it checks the interrupt flag, and that delays close().

Take quiz
How does a scope stop running subtasks when cancelled?
It calls Thread.stop()
It kills the JVM process
It sets a flag the Joiner polls
It interrupts their threads
A fork() call made after the scope is cancelled results in:
A subtask that stays UNAVAILABLE and does not run
A subtask that runs normally
An immediate FailedException
A subtask in SUCCESS state with null

27. How do you set a scope name and thread factory?

Both go through the Configuration object passed to open(). withName(String) labels the scope, and withThreadFactory(ThreadFactory) controls how subtask threads are created.

try (var scope = StructuredTaskScope.open(
        Joiner.<Profile>allSuccessfulOrThrow(),
        cf -> cf.withName("profile-load")
                .withThreadFactory(Thread.ofVirtual().name("pf-", 0).factory()))) {
    // fork tasks
}

A scope name shows up in JSON thread dumps, so choose something that identifies the operation, not a generic label.

Take quiz
Where is withName() called?
On the Configuration passed to open()
On the Subtask
On the Joiner
On Thread.currentThread()
Why give a scope a meaningful name?
It speeds up fork()
It appears in structured thread dumps
It raises thread priority
It is required by preview mode

28. Why can only the owner thread call join() and close()?

A scope represents one block of code in one thread. If any thread could join or close it, the link between the scope's lifetime and that block would break, and the guarantee that subtasks never outlive their parent would be lost.

The restriction is enforced at run time with WrongThreadException. Subtasks can still do useful work, and threads inside the scope can fork more subtasks into it, but the end of the scope stays under the owner's control.

This mirrors how a local variable or a try block belongs to one thread of control.

Take quiz
What does the owner-only rule on join() and close() protect?
Memory usage of virtual threads
The tie between scope lifetime and one block of code
The size of the thread pool
Class loading order
What is thrown when a non-owner calls close()?
IllegalArgumentException
ConcurrentModificationException
WrongThreadException
UnsupportedOperationException

29. What happens if you forget to call join()?

When the try block ends, close() sees that subtasks were forked but join() was never called. It cancels the subtasks, waits for them, and then throws IllegalStateException to report the bug.

try (var scope = StructuredTaskScope.open()) {
    var t = scope.fork(() -> work());
    // join() missing
    return t.get();      // IllegalStateException here already
}

Even before close, get() throws because results are only visible after a join. The scope still cleans up, so you get an error instead of a silent leak.

Take quiz
What does close() do if fork() was used but join() never called?
Silently ignores it
Calls join() for you
Cancels subtasks and throws IllegalStateException
Throws FailedException
Why does get() fail if join() has not been called?
get() needs a timeout argument
The thread factory blocks it
The Subtask is still being compiled
Results are only available after the owner joins

30. What is StructureViolationException?

It is thrown when the nesting rules for scopes or scoped values are broken. The runtime keeps a stack of scopes per thread and checks that they are closed in the reverse order they were opened.

A typical cause is closing an outer scope while an inner one is still open, or leaking a scope out of the thread that opened it. It signals a bug in how scopes are used, not a failure of a subtask.

var outer = StructuredTaskScope.open();
var inner = StructuredTaskScope.open();
outer.close();   // inner still open -> StructureViolationException

Take quiz
What does StructureViolationException indicate?
A subtask returned null
A network timeout
A joiner returned the wrong type
Scopes were used out of their required nesting order
Which sequence is most likely to trigger it?
Closing an outer scope before an inner one
Calling fork() twice
Calling join() after close()
Using a Callable instead of Runnable

31. How do scoped values work with structured concurrency?

A ScopedValue binds an immutable value for the duration of a call. Threads forked by a StructuredTaskScope automatically inherit the owner's bindings, so request data such as a user or trace ID flows to subtasks without parameters.

static final ScopedValue<User> USER = ScopedValue.newInstance();

ScopedValue.where(USER, currentUser).run(() -> {
    try (var scope = StructuredTaskScope.open()) {
        scope.fork(() -> audit(USER.get()));   // sees currentUser
        scope.join();
    }
});

Bindings disappear when the run call returns, matching the scope's lifetime. ScopedValue is final in Java 25 even though the scope is preview.

Take quiz
What do subtasks forked in a scope see for a bound ScopedValue?
The owner's binding, inherited automatically
Always an empty value
A mutable copy per thread
Only values set with set()
When does a ScopedValue binding end?
When the JVM exits
When the where(...).run(...) call returns
When the garbage collector runs
When the first subtask finishes

32. Why are ThreadLocals discouraged with subtasks?

Each subtask runs on a new thread, so a ThreadLocal set by the owner is not visible there. Copying values through InheritableThreadLocal adds per-thread cost, and the copies are mutable, which makes sharing state risky.

A thread-local also has no clear end of life. It lives until the thread dies or someone calls remove(), and that mismatches the short, scoped life of a subtask.

ScopedValue fixes this. It is immutable, inherited cheaply by structured subtasks, and unbound when the block ends.

Take quiz
Why does a ThreadLocal set in the owner thread not reach a subtask?
Subtasks run in the same thread
The subtask runs in a different, new thread
ThreadLocal is removed in Java 25
Virtual threads cannot use ThreadLocal
Which feature is preferred for passing context into subtasks?
Static mutable fields
System properties
ScopedValue
ThreadGroup

33. How do nested scopes behave in structured concurrency?

A subtask can open its own scope. The inner scope must be closed before the subtask ends, and the subtask itself must finish before the outer scope closes. The result is a tree of scopes that matches the call structure.

flowchart TD
    A["Owner opens outer scope"] --> B["fork subtask 1"]
    A --> C["fork subtask 2"]
    B --> D["Subtask 1 opens inner scope"]
    D --> E["fork leaf A"]
    D --> F["fork leaf B"]

Cancelling the outer scope interrupts subtask 1. Its join() is interrupted, its inner scope closes, and the leaf tasks are cancelled in turn, so cancellation moves down the tree.

Take quiz
When the outer scope is cancelled, what happens to an inner scope inside a subtask?
The inner scope keeps running
The inner scope is left open
The subtask is interrupted and closing its inner scope cancels the leaves
The JVM exits
In which order must nested scopes be closed?
Outer first, then inner
Any order
Alphabetical by name
Inner first, then outer

34. What is allUntil() and when should you use it?

Joiner.allUntil(Predicate<Subtask<T>>) runs subtasks until the predicate returns true for a completed subtask, then cancels the rest. join() returns a List<Subtask<T>> of all forked subtasks, so you inspect the state of each.

var count = new AtomicInteger();
try (var scope = StructuredTaskScope.open(
        Joiner.<Hit>allUntil(st -> st.state() == Subtask.State.SUCCESS
                                   && count.incrementAndGet() >= 3))) {
    shards.forEach(s -> scope.fork(() -> s.search(q)));
    List<Subtask<Hit>> all = scope.join();
}

Use it when you need a custom stop rule, such as 'stop after 3 successes', and want to see unfinished subtasks as UNAVAILABLE.

Take quiz
What does join() return with Joiner.allUntil()?
A single result
null
A Map of results by thread name
A List of Subtask objects
When does allUntil() cancel remaining subtasks?
When the predicate returns true
After a fixed 1 second
Only on exception
Never

35. What is the difference between a Joiner and the old ShutdownOnFailure?

Java 21-24 style Java 25 style
Policy Subclass the scope Pass a Joiner
Creation new ShutdownOnFailure() open(joiner)
Failure check throwIfFailed() after join join() throws FailedException
Result From Subtask handles Returned by join()
Deadline joinUntil(Instant) withTimeout(Duration)

The old classes baked a policy into inheritance, and custom policies meant subclassing a scope. A Joiner is a small, separate object, easy to compose and unit-test.

Take quiz
Which replaced throwIfFailed() for failure reporting in Java 25?
join() throwing FailedException
scope.check()
Subtask.rethrow()
Thread.getUncaughtExceptionHandler()
How is a policy chosen in Java 25 compared to Java 21?
By subclassing StructuredTaskScope
By passing a Joiner to open()
By setting a system property
By annotating the method

36. How do you write a custom Joiner?

Implement StructuredTaskScope.Joiner<T, R>. Override onComplete to handle each finished subtask and return true when you want to cancel the scope. Override result() to produce the value join() returns.

class FirstN<T> implements StructuredTaskScope.Joiner<T, List<T>> {
    private final int n;
    private final Queue<T> results = new ConcurrentLinkedQueue<>();

    FirstN(int n) { this.n = n; }

    @Override
    public boolean onComplete(StructuredTaskScope.Subtask<? extends T> st) {
        if (st.state() == StructuredTaskScope.Subtask.State.SUCCESS) {
            results.add(st.get());
        }
        return results.size() >= n;      // true -> cancel the rest
    }

    @Override
    public List<T> result() { return List.copyOf(results); }
}

onComplete is called from subtask threads, so the joiner's state must be thread-safe. Keep it fast. It runs inside the subtask's completion path.

Take quiz
Why must a custom Joiner's internal state be thread-safe?
join() runs on every thread
onComplete is invoked from the subtask threads
Joiner is serialized to disk
The JVM requires it for preview features
In a custom Joiner, how do you stop the scope early?
Call System.exit()
Throw InterruptedException from result()
Return true from onComplete
Call Thread.stop() on the owner

37. What happens when one subtask throws while others are still running?

With a failure-aware joiner, the failed subtask's completion triggers onComplete. The joiner records the exception and returns true, so the scope is cancelled and the sibling threads are interrupted.

sequenceDiagram
    participant O as Owner thread
    participant S as Scope
    participant A as Subtask A
    participant B as Subtask B
    O->>S: fork A and B, then join()
    A--xS: throws exception
    S->>B: interrupt (cancel)
    S-->>O: join() throws FailedException(cause)
    O->>S: close() waits for B to exit

Exceptions thrown by siblings after cancellation are not reported. Only the first failure becomes the cause. The owner wakes up in join(), and close() still waits for interrupted siblings to finish.

Take quiz
Which failure becomes the cause of FailedException with allSuccessfulOrThrow()?
The last subtask failure
All failures merged into one
The first subtask failure
A random subtask failure
After the first failure cancels the scope, what does close() do?
Returns immediately without waiting
Restarts them
Forces them with Thread.stop()
Waits for interrupted siblings to exit

38. What happens if the owner thread is interrupted during join()?

join() throws InterruptedException and the scope is cancelled, so running subtasks are interrupted too. If the exception propagates out of the try block, close() waits for them to finish and the interrupt flag effectively moves down the tree.

try (var scope = StructuredTaskScope.open()) {
    scope.fork(() -> work());
    scope.join();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();   // restore the flag
    throw new RuntimeException("cancelled", e);
}

If you catch and swallow the exception, restore the interrupt flag so callers further up still see the cancellation request.

Take quiz
What does join() throw when the owner thread is interrupted?
TimeoutException
WrongThreadException
CancellationException
InterruptedException
If you catch InterruptedException from join(), what should you usually do?
Restore the interrupt flag or rethrow
Ignore it completely
Call join() in a loop
Call System.exit()

39. How do you return partial results when one subtask is slow?

Pick Joiner.awaitAll() so a failure does not cancel the scope, and give each subtask its own fallback or deadline inside the task itself.

try (var scope = StructuredTaskScope.open(Joiner.<Optional<Rec>>awaitAll())) {
    var a = scope.fork(() -> safe(() -> recs.fromA(id), Duration.ofMillis(300)));
    var b = scope.fork(() -> safe(() -> recs.fromB(id), Duration.ofMillis(300)));
    scope.join();
    return Stream.of(a, b)
                 .filter(t -> t.state() == Subtask.State.SUCCESS)
                 .map(Subtask::get)
                 .flatMap(Optional::stream)
                 .toList();
}

Here safe(...) catches errors and timeouts and returns Optional.empty(). A scope-wide timeout is a poor fit because it cancels everything and join() throws, discarding the fast results.

Take quiz
Which joiner suits a scope where some failures are acceptable?
awaitAll()
anySuccessfulOrThrow()
allSuccessfulOrThrow()
awaitAllSuccessfulOrThrow()
Why is a scope-wide timeout a poor way to get partial results?
It slows down fast subtasks
It cancels all subtasks and join() throws
It is not supported with awaitAll()
It converts results to null

40. Why should subtask code handle interruption correctly?

Cancellation in a scope is delivered as an interrupt. A subtask that ignores it keeps running, and because close() waits for every thread, the owner can hang after a failure or timeout.

  • Let InterruptedException propagate, or restore the flag with Thread.currentThread().interrupt().
  • In long CPU loops, check Thread.currentThread().isInterrupted() regularly.
  • Prefer interruptible APIs, such as timeouts on HTTP clients and queue poll with a timeout.
scope.fork(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        if (processNextChunk()) break;
    }
    return null;
});

Take quiz
What can happen if a subtask ignores interruption?
The JVM kills it instantly
close() waits on it and the owner can hang
The scope ignores cancellation
It restarts automatically
In a long CPU-bound subtask loop, how do you honor cancellation?
Call System.gc()
Catch Error
Check Thread.currentThread().isInterrupted()
Poll scope.state()

41. How do you troubleshoot structured concurrency with thread dumps?

Use the JSON thread dump format. Unlike the plain-text dump, it groups threads by the container that owns them, so you can see which scope a subtask belongs to and how scopes nest.

jcmd <pid> Thread.dump_to_file -format=json /tmp/dump.json

Look for the scope's name (set with withName), its owner thread, and the list of subtask threads under it. A scope with many blocked subtasks and an owner parked in join() usually means one subtask ignores interruption or a downstream call has no timeout.

Give scopes descriptive names and subtask threads a naming factory to make dumps easy to read.

Take quiz
Which jcmd command writes a thread dump that shows scope hierarchy?
jcmd <pid> VM.flags
jcmd <pid> GC.run
jcmd <pid> Thread.dump_to_file -format=json file
jcmd <pid> Thread.print -l
What does an owner parked in join() with many blocked subtasks usually suggest?
The scope has finished
The JVM is out of metaspace
The scope was never opened
A subtask is stuck, for example a call without a timeout

42. How does structured concurrency compare with CompletableFuture?

Aspect CompletableFuture StructuredTaskScope
Style Callback chains Plain blocking code
Error handling exceptionally, handle try/catch around join()
Cancellation Not propagated to siblings Automatic via joiner
Lifetime Free-floating Bound to a block
Stack traces Fragmented across stages Natural, one thread per task

CompletableFuture is still useful for asynchronous pipelines that cross method boundaries. For fan-out inside one method, a scope is easier to read and debug, and it cannot leak tasks.

Take quiz
Which has automatic sibling cancellation on failure?
CompletableFuture.allOf
ExecutorService.invokeAll
Plain Thread.start()
StructuredTaskScope with a failure-aware joiner
How are errors typically handled around a scope?
try/catch around join()
exceptionally() stages
A global error callback
Future.get() on each stage

43. How do you test code that uses StructuredTaskScope?

First enable preview for the test JVM, for example in Maven Surefire: <argLine>--enable-preview</argLine>. Then test the three paths separately: success, failure and cancellation.

@Test
void failurePropagatesAndCancelsSibling() {
    var interrupted = new CountDownLatch(1);
    assertThrows(StructuredTaskScope.FailedException.class, () -> {
        try (var scope = StructuredTaskScope.open()) {
            scope.fork(() -> { throw new IllegalStateException("boom"); });
            scope.fork(() -> {
                try { Thread.sleep(5_000); }
                catch (InterruptedException e) { interrupted.countDown(); throw e; }
                return null;
            });
            scope.join();
        }
    });
    assertEquals(0, interrupted.getCount());   // sibling was cancelled
}

Use latches instead of sleeps to keep tests deterministic, and inject the work so you can simulate slow or failing subtasks.

Take quiz
What does the latch in the example confirm?
The sibling subtask was interrupted
The scope timed out
join() was never called
The test used platform threads
Which Surefire setting is needed for tests using the preview API?
skipTests=true
argLine with --enable-preview
forkCount=0
reuseForks=preview

44. How can you limit concurrency inside a StructuredTaskScope?

A scope starts one virtual thread per fork, so forking 10,000 tasks starts 10,000 threads. That is cheap for the JVM, but a database or remote API may not cope. The standard answer is a Semaphore, which limits only the part that needs it.

var permits = new Semaphore(20);
try (var scope = StructuredTaskScope.open(Joiner.<Resp>allSuccessfulOrThrow())) {
    for (String url : urls) {
        scope.fork(() -> {
            permits.acquire();
            try { return http.get(url); }
            finally { permits.release(); }
        });
    }
    return scope.join();
}

Do not use a fixed-size platform thread pool to throttle. It throws away the advantage of virtual threads and brings back queueing.

Take quiz
What is the recommended way to throttle calls to a downstream service?
A fixed platform thread pool
A Semaphore around the downstream call
Forking fewer tasks than data items
Setting a smaller timeout
Why not throttle by using a small platform thread pool for subtasks?
It is not allowed by the compiler
It disables join()
It discards the benefits of virtual threads
It breaks ScopedValue

45. Why does synchronized pinning matter for subtasks?

Subtasks run on virtual threads. Before JDK 24, blocking inside a synchronized block pinned the virtual thread to its carrier thread, so it could not be unmounted. With many subtasks this could exhaust the carrier threads and stall the application.

JEP 491, delivered in Java 24 and present in Java 25, removed pinning for synchronized. Blocking in monitors now releases the carrier thread.

Pinning can still happen in native frames or foreign function calls. If subtasks stall for no obvious reason, check for blocking native calls, or run with -Djdk.tracePinnedThreads=full to see where pinning occurs.

Take quiz
Which JEP removed virtual thread pinning for synchronized?
JEP 505
JEP 506
JEP 491
JEP 444
Which case can still pin a virtual thread in Java 25?
Blocking inside synchronized
Calling Thread.sleep
Using ReentrantLock
Blocking inside a native frame

46. How do you unwrap FailedException to handle the real error?

Catch StructuredTaskScope.FailedException and inspect getCause(). Convert it to a domain exception at the boundary of your method so callers do not need to know about the scope.

try (var scope = StructuredTaskScope.open(Joiner.<Order>allSuccessfulOrThrow())) {
    // fork...
    return scope.join();
} catch (StructuredTaskScope.FailedException e) {
    Throwable cause = e.getCause();
    if (cause instanceof PaymentDeclined pd) throw pd;
    if (cause instanceof RuntimeException re) throw re;
    throw new OrderFailed(cause);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new OrderFailed(e);
}

Do not log the wrapper and the cause separately, or you will produce duplicate entries.

Take quiz
Where do you find the real subtask error in FailedException?
getSuppressed()[0]
scope.errors()
Thread.currentThread().getName()
getCause()
Why convert FailedException into a domain exception?
To hide the concurrency mechanism from callers
To speed up join()
To satisfy the compiler
To enable preview features

47. When would you choose structured concurrency over parallel streams?

Parallel streams StructuredTaskScope
Workload CPU-bound data parallelism I/O-bound fan-out
Tasks Same operation on many elements Different operations allowed
Threads Common ForkJoinPool Virtual thread per subtask
Cancellation on failure Not coordinated Built in via joiner
Timeout None built in withTimeout

Use a scope when you call a few services in parallel and care about failure, cancellation and timeouts. Use parallel streams for number crunching over large in-memory collections.

Take quiz
Which workload fits StructuredTaskScope best?
Calling several remote services in parallel
Sorting a large in-memory array
Summing an int array
Mapping a million CPU-heavy elements
What do parallel streams run on by default?
One virtual thread per element
The common ForkJoinPool
A scope's thread factory
The owner thread only

48. Explain the lifecycle of a StructuredTaskScope?

A scope goes through a fixed sequence, and the compiler and runtime both help keep it in order.

flowchart LR
    A[open] --> B["fork subtasks"]
    B --> C[join]
    C --> D["read results"]
    D --> E[close]
    C -. failure or timeout .-> F["cancel and interrupt"]
    F --> E
  1. Open: open(joiner, config) creates the scope and makes the current thread its owner.
  2. Fork: each fork starts a subtask thread and calls joiner.onFork.
  3. Join: the owner waits. Each completion calls joiner.onComplete, which may cancel the scope.
  4. Read: results come from join() or Subtask.get().
  5. Close: unfinished subtasks are interrupted and awaited, and the scope ends.
Take quiz
In which order do the lifecycle stages happen?
fork, open, close, join
open, fork, join, read results, close
open, join, fork, close
close, open, fork, join
Which step is responsible for waiting for interrupted subtasks to actually exit?
fork()
open()
close()
Subtask.state()

49. Explain the execution flow of a fan-out request using a scope?

Say a handler needs a user profile and recent orders to build one response. The handler opens a scope, forks both lookups, and blocks in join().

sequenceDiagram
    participant H as Handler (owner)
    participant S as Scope
    participant U as User service
    participant O as Order service
    H->>S: open(allSuccessfulOrThrow)
    H->>S: fork(loadUser)
    S->>U: run in virtual thread 1
    H->>S: fork(loadOrders)
    S->>O: run in virtual thread 2
    H->>S: join() blocks
    U-->>S: Profile
    O-->>S: List of orders
    S-->>H: List of results
    H->>S: close()

Both lookups run at once, so latency is close to the slower call, not the sum. If one fails, the other is interrupted and the handler gets a FailedException. No lookup can keep running after the handler returns.

Take quiz
In a fan-out of two calls, roughly what is the response latency?
The sum of both calls
Always the faster call
About the slower of the two calls
Fixed by the thread pool size
What does the handler thread do right after forking both lookups?
Closes the scope
Calls get() immediately
Starts a third thread manually
Blocks in join() until the scope completes

50. How do you migrate from the Java 21-24 StructuredTaskScope API to Java 25?

Java 21-24 Java 25
new StructuredTaskScope.ShutdownOnFailure() StructuredTaskScope.open() or open(Joiner.allSuccessfulOrThrow())
new StructuredTaskScope.ShutdownOnSuccess<T>() open(Joiner.anySuccessfulOrThrow())
scope.throwIfFailed() join() throws FailedException
scope.joinUntil(deadline) cf.withTimeout(Duration)
ExecutionException on failure FailedException
scope.shutdown() Joiner returns true to cancel

// Before (Java 21-24)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var a = scope.fork(() -> loadA());
    scope.join().throwIfFailed();
    return a.get();
}

// After (Java 25)
try (var scope = StructuredTaskScope.open()) {
    var a = scope.fork(() -> loadA());
    scope.join();
    return a.get();
}

Also review catch blocks, since the exception type changed, and replace any custom subclasses with a Joiner implementation.

Take quiz
What replaces new ShutdownOnSuccess<T>() in Java 25?
open(Joiner.awaitAll())
StructuredTaskScope.race()
open(Joiner.allUntil(null))
open(Joiner.anySuccessfulOrThrow())
Which exception type replaces ExecutionException for scope failures?
StructuredTaskScope.FailedException
CompletionException
ScopeException
StructureViolationException
«
»

Comments & Discussions