Walk through the lifecycle of a StructuredTaskScope: what do fork(), join(), Subtask.get(), and close() each do, and in what order must they be called?
answer
- order: open -> fork -> join -> get -> close
- fork: non-blocking, owner thread only, returns Subtask handle
- join: blocks owner until all done (or policy stops early)
- get: only after join, only on SUCCESS, else IllegalStateException
- close: interrupts stragglers, waits for all = no orphans
basics
~20 sOpen the scope (usually in try-with-resources). fork() starts each subtask and returns a handle without blocking. join() waits for all subtasks to finish. After join, get() reads a successful subtask's result. close() (automatic) shuts the scope and waits for everything to stop.
solid answer
~50 sThe lifecycle is open, fork, join, read, close. You create the scope, typically inside try-with-resources so close() always runs. Each fork(Callable) submits a subtask onto a new (virtual) thread and immediately returns a Subtask handle; it never blocks the caller, so multiple forks run concurrently. fork() must be called from the scope's owner thread. join() blocks the owner until every forked subtask has completed, or until the scope's policy decides to stop early; it must be called before you read any result. After a successful join, Subtask.get() returns the value of a subtask whose state is SUCCESS — calling get() before join, or on a failed/unavailable subtask, throws IllegalStateException. Finally close() (run automatically by try-with-resources on any exit path) shuts the scope down, interrupts any still-running subtasks, and blocks until they all terminate, enforcing that nothing leaks. The strict ordering — fork on owner, then join, then get, then close — is what makes the structure safe and observable.
code
java · 8 linestry (var scope = new StructuredTaskScope<Object>()) {
Subtask<User> user = scope.fork(() -> fetchUser(id)); // non-blocking
Subtask<Order> order = scope.fork(() -> fetchOrder(id)); // runs concurrently
scope.join(); // block owner until both finish
return new Response(user.get(), order.get()); // get() legal only after join, on SUCCESS
} // close() runs here on every exit path: interrupts stragglers, waits, no orphansgo deeper
Can recite open-fork-join-get-close and that fork doesn't block while join does.
Explains each method's contract, the read-after-join rule, owner-thread restriction, and what close() guarantees.
Knows subtask states (UNAVAILABLE/SUCCESS/FAILED), the IllegalStateException guards, and how policies can unblock join early.
Reasons about the invariants the ordering enforces and the API evolution across JDK previews when planning adoption and code reviews.
## The four operations A `StructuredTaskScope` has a well-defined lifecycle. Think of the **owner thread** as the thread that creates the scope and runs the enclosing block. ### 1. Open the scope ```java try (var scope = new StructuredTaskScope<T>()) { ... } ``` The `try (...)` is **try-with-resources**: any object that implements `AutoCloseable` declared there gets its `close()` called automatically when the block exits, on *every* path (normal return, exception, break). `StructuredTaskScope` is `AutoCloseable`, so this is how we guarantee cleanup. ### 2. `fork(Callable<U>)` — start a subtask ```java Subtask<User> u = scope.fork(() -> fetchUser(id)); Subtask<Order> o = scope.fork(() -> fetchOrder(id)); ``` - A **`Callable`** is a function that returns a value and may throw. - `fork` starts the callable on a **new thread** (a virtual thread by default) and returns a **`Subtask`** — a handle to that running work. - **`fork` does not block.** Control returns immediately, so the second `fork` runs concurrently with the first. - `fork` must be called by the **owner thread** only (this is part of the structure — children don't fork into a parent's scope arbitrarily). A `Subtask` has a **state**: `UNAVAILABLE` (not done), `SUCCESS` (completed with a value), or `FAILED` (threw). ### 3. `join()` — wait for all subtasks ```java scope.join(); ``` `join()` blocks the **owner** until **every** forked subtask has terminated — or until the scope's **policy** (e.g. shutdown-on-failure / shutdown-on-success) decides to stop early and unblocks `join` sooner. `join()` may throw `InterruptedException` (the owner was interrupted). You **must** call `join()` before reading any result. A scope that forks but never joins, or reads before joining, is a programming error. ### 4. `Subtask.get()` — read a result ```java return process(u.get(), o.get()); ``` After a successful `join()`, `get()` returns the result of a subtask **whose state is `SUCCESS`**. The critical rule: **`get()` is only legal after `join()` and only on a successful subtask.** Calling it earlier, or on a `FAILED`/`UNAVAILABLE` subtask, throws `IllegalStateException` — this is deliberate, so you can't accidentally read a half-baked or failed result. (To inspect failures you use the subtask's `state()`/`exception()`, or you let the policy propagate the error — see the shutdown-policy question.) ### 5. `close()` — shut down and reap `close()` (called automatically by try-with-resources) **shuts the scope down**: it signals cancellation/interrupt to any subtask still running and **blocks until they all terminate**. This is the linchpin of the no-orphans guarantee — the block physically cannot finish while a child is alive. If you forgot to `join()`, `close()` still cleans up, but well-formed code always joins first. ## The mandatory order ``` open -> fork* (owner thread, non-blocking) -> join() -> get()/read -> close() (auto) ``` Violating it is caught: read-before-join throws `IllegalStateException`; fork-from-non-owner is rejected; missing close is prevented by try-with-resources. ## Why each rule exists - **fork non-blocking** → real concurrency (otherwise it'd be sequential). - **join before read** → results are actually ready and the policy has had its say. - **get only on SUCCESS** → no silent reading of failed/unfinished work. - **close waits** → the structural invariant: lifetimes nest, nothing leaks. ## Note on API evolution Across JDK 21–25 previews the surface shifted (e.g. scope creation via factory/`open(...)` with a `Joiner`, `Subtask.get()` vs older `resultNow()`), but the *lifecycle and ordering rules* are the stable, examinable core.
- What happens if you call Subtask.get() before join()?It throws IllegalStateException — get() is only valid after join() and only on a subtask whose state is SUCCESS. This guard prevents reading unfinished or failed results.
- Does fork() block the caller?No. fork() starts the subtask on a new (virtual) thread and returns its Subtask handle immediately, so subsequent forks run concurrently. Blocking happens at join().
saying these in an interview costs you the question
- Believing fork() blocks until the subtask completes.
- Calling Subtask.get() before join() (throws IllegalStateException).
- Thinking join() returns results — it returns nothing; you read via get() afterward.
- Assuming any thread can fork into the scope — only the owner thread may fork.
- Skipping try-with-resources and forgetting close(), risking leaked subtasks.