Add structured-concurrency-final module: StructuredTaskScope final in JDK 28 (JEP 543), run on 28-ea, 27, 26 and 25

Runnable demos for the ankurm.com structured-concurrency-java article: the ExecutorService thread leak and its fix, every built-in Joiner,
structure violations, timeouts, a custom Joiner, JSON thread dumps (in-process and jcmd), scoped values, javap diffs of the API on 25, 26, 27 and 28-ea,
compiler output for six pieces of 25/26-era code, and the preview class-file trap between 27 and 28. Transcripts in output/.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz
This commit is contained in:
Claude
2026-10-26 02:01:31 +00:00
parent 2e25042a68
commit c93a1fffbb
37 changed files with 1351 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
# structured-concurrency-final — Structured Concurrency, final in JDK 28 (JEP 543)
Companion code for the ankurm.com article **Structured Concurrency Is Final in JDK 28 (JEP 543): From Preview to Production**
(<https://ankurm.com/structured-concurrency-java/>). The explanation lives in the article, in collapsible sections; this folder holds the runnable
sources and the transcripts they produced.
```bash
JDK25=/path/to/jdk-25 JDK26=/path/to/jdk-26 JDK27=/path/to/jdk-27 JDK28=/path/to/jdk-28 ./run.sh # regenerates output/*.txt
```
## What was tested against
| Thing | Build | Note |
|---|---|---|
| JDK 28 | Temurin `28-beta+21-ea` (28-ea+21, build of 2026-10-22) | **early access**; JEP 543 status "Integrated", GA proposed 2027-03-09. No flags needed |
| JDK 27 | Temurin 27.0.1.0.2+1 | GA 2026-09-15. Seventh preview (JEP 533): needs `--enable-preview` |
| JDK 26 | Temurin 26.0.2.1+1 | sixth preview (JEP 525), for the old-API compile results |
| JDK 25 | Temurin 25.0.5+8 | fifth preview (JEP 505), for the API diffs |
`javap` prints identical `StructuredTaskScope` signatures on 27 and on 28-ea+21 (`output/09-api-diff.txt`). Nothing here was run on a GA JDK 28, because none exists yet.
## Layout
- `src/` — runnable demos, no flags on JDK 28: `Server` (the canonical program), `Unstructured` and `FailFast` (the thread leak and its fix),
`Joiners` (the built-in joiners), `Failures` (structure violations and failure handling), `Timeouts`, `CollectingJoiner` (a custom joiner),
`RandomTasks` (seeded), `StuckClose` (a subtask that ignores interrupts), `ThreadDump` (JSON thread dump with the scope hierarchy),
`ScopedValues`, `Snippets` and `QuickReference` (the short fragments quoted in the article).
- `broken/` — six sources that compile on JDK 26 and fail on 28: `MissingCatch`, `OldFailedException`, `OldTimeoutException`, `AwaitAll`, `OldJoiner`, `ShutdownOnFailure`.
- `output/` — one transcript per script step; `run.sh` regenerates all of them.
| Transcript | What it shows |
|---|---|
| `01-server.txt` | the canonical program on 28 (no flags), on 27 (`--enable-preview`), and the 27 compile errors without the flag |
| `02-unstructured-vs-structured.txt` | `ExecutorService` leaks the sibling; `StructuredTaskScope` interrupts it |
| `03-joiners.txt` | `allSuccessfulOrThrow`, `anySuccessfulOrThrow`, `awaitAllSuccessfulOrThrow`, `allUntil`, the `Function` overloads |
| `04-failures.txt` | `get()` before `join()`, no `join()`, non-owner `fork`, out-of-order close, join twice, cause pattern matching |
| `05-timeouts-and-custom-joiner.txt` | scope timeouts, a partial-result `timeout()`, thread factory, a custom joiner |
| `06-thread-dump.txt`, `15-jcmd-thread-dump.txt` | the scope as a container in a JSON thread dump (in-process and via `jcmd`) |
| `07-scoped-values.txt` | subtasks inherit a `ScopedValue` binding, a plain thread does not |
| `08-api-28.txt`, `09-api-diff.txt` | `javap` of the API on 28, and the diffs 25 to 26 to 27 to 28 |
| `10-broken-old-api.txt` | the compiler's messages for 25/26-era code on 28 |
| `11-preview-traps.txt` | a class compiled with preview on 27 does not load on 28; `--release 27` on a 28 compiler |
| `12-snippets.txt` | the short fragments in the article compile and run |
| `13-random-tasks.txt`, `14-stuck-close.txt` | seeded fan-out with one slow task; a subtask that ignores interrupts |
Everything is byte-stable between runs (timings are reduced to booleans). The JEP statements (what changed in each preview) come from the JEP texts on openjdk.org;
only the 25 to 28 API differences were checked against binaries.
@@ -0,0 +1,12 @@
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
/** Joiner.awaitAll() existed in 25 and 26 and was removed in 27. */
public class AwaitAll {
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll())) {
scope.fork(() -> "x");
scope.join();
}
}
}
@@ -0,0 +1,13 @@
import java.util.concurrent.StructuredTaskScope;
/** Valid on JDK 25 and 26 (join() threw only InterruptedException). On 27 and 28 join() can throw ExecutionException. */
public class MissingCatch {
static String handle() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> "Alice");
scope.join();
return user.get();
}
}
public static void main(String[] args) throws Exception { System.out.println(handle()); }
}
@@ -0,0 +1,13 @@
import java.util.concurrent.StructuredTaskScope;
/** The JDK 25/26 way to catch a failed scope. FailedException is gone in 27. */
public class OldFailedException {
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open()) {
scope.fork(() -> { throw new IllegalStateException("boom"); });
scope.join();
} catch (StructuredTaskScope.FailedException e) {
System.out.println("cause: " + e.getCause());
}
}
}
@@ -0,0 +1,12 @@
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** A custom Joiner as written for JDK 26: two type parameters and onTimeout(). */
public class OldJoiner<T> implements Joiner<T, List<T>> {
private final java.util.Queue<T> results = new ConcurrentLinkedQueue<>();
public boolean onComplete(Subtask<T> s) { if (s.state() == Subtask.State.SUCCESS) results.add(s.get()); return false; }
public void onTimeout() { }
public List<T> result() { return List.copyOf(results); }
}
@@ -0,0 +1,15 @@
import java.time.Duration;
import java.util.concurrent.StructuredTaskScope;
/** The JDK 25/26 timeout exception. In 27 the scope throws ExecutionException with a CancelledByTimeoutException cause. */
public class OldTimeoutException {
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open(StructuredTaskScope.Joiner.<String>awaitAllSuccessfulOrThrow(),
cf -> cf.withTimeout(Duration.ofMillis(50)))) {
scope.fork(() -> { Thread.sleep(1000); return "late"; });
scope.join();
} catch (StructuredTaskScope.TimeoutException e) {
System.out.println("timed out");
}
}
}
@@ -0,0 +1,12 @@
import java.util.concurrent.StructuredTaskScope;
/** The JDK 21-24 API that older tutorials still show. It is not in 25 or later. */
public class ShutdownOnFailure {
public static void main(String[] args) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> "Alice");
scope.join().throwIfFailed();
System.out.println(user.get());
}
}
}
@@ -0,0 +1,35 @@
$ javac src/Server.java && java Server (28-beta+21-ea)
Response[user=Alice, order=42]
Runtime: 28-beta+21-ea
$ javac --enable-preview --release 27 src/Server.java && java --enable-preview Server (27.0.1.0.2+1)
Response[user=Alice, order=42]
Runtime: 27.0.1.0.2+1
$ javac src/Server.java (27.0.1.0.2+1)
src/Server.java:2: error: StructuredTaskScope is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope;
^
(use --enable-preview to enable preview APIs)
src/Server.java:3: error: StructuredTaskScope is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope.Subtask;
^
(use --enable-preview to enable preview APIs)
src/Server.java:3: error: Subtask is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope.Subtask;
^
(use --enable-preview to enable preview APIs)
src/Server.java:22: error: StructuredTaskScope is a preview API and is disabled by default.
try (var scope = StructuredTaskScope.open()) { // 1. open
^
(use --enable-preview to enable preview APIs)
src/Server.java:23: error: Subtask is a preview API and is disabled by default.
Subtask<String> user = scope.fork(this::findUser); // 2. fork
^
(use --enable-preview to enable preview APIs)
src/Server.java:24: error: Subtask is a preview API and is disabled by default.
Subtask<Integer> order = scope.fork(this::fetchOrder); // fork
^
(use --enable-preview to enable preview APIs)
6 errors
exit=1
@@ -0,0 +1,10 @@
$ javac src/Unstructured.java && java Unstructured (28-beta+21-ea)
handle() failed: java.lang.IllegalStateException: user service down
handle() has returned. Waiting 400 ms to see what the other thread does:
fetchOrder() finished, although nobody wants the answer any more
$ javac src/FailFast.java && java FailFast (28-beta+21-ea)
fetchOrder() was interrupted
handle() failed: java.lang.IllegalStateException: user service down
handle() has returned. Waiting 400 ms to see what the other thread does:
(nothing more happened)
@@ -0,0 +1,21 @@
$ javac src/Joiners.java && java Joiners (28-beta+21-ea)
-- allSuccessfulOrThrow(): List in FORK order, not completion order
[slow, fast, medium]
-- allSuccessfulOrThrow(): one failure cancels the others, join() throws ExecutionException
ExecutionException, cause = java.lang.IllegalStateException: broken failed
interrupted: [slow]
-- anySuccessfulOrThrow(): the race; losers are interrupted
winner: B-50ms
interrupted: [A-300ms, C-300ms]
-- anySuccessfulOrThrow(): when every subtask fails
ExecutionException, cause = java.lang.IllegalStateException: one failed
-- awaitAllSuccessfulOrThrow(): returns null (Void), like the zero-argument open()
join() = null, a=a, b=b
-- allUntil(predicate): stop when two subtasks have completed
SUCCESS t1
SUCCESS t2
UNAVAILABLE
UNAVAILABLE
interrupted: [t3, t4]
-- the Function overload: join() throws OUR exception type
caught ServiceException: service call failed: orders failed
@@ -0,0 +1,15 @@
$ javac src/Failures.java && java Failures (28-beta+21-ea)
-- Subtask.get() before join()
java.lang.IllegalStateException: null
-- leaving the try block without join()
java.lang.IllegalStateException: Owner did not join after forking
-- fork() from a thread that does not own the scope
returned java.lang.WrongThreadException: Current thread not owner
-- closing scopes out of order (not try-with-resources)
java.util.concurrent.StructureViolationException: null
-- join() twice
java.lang.IllegalStateException: Already joined or scope is closed
-- the failure surfaces as ExecutionException; pattern-match the cause
I/O failed: disk gone
-- two subtasks fail: which cause do you get?
cause = first (the first to fail wins)
@@ -0,0 +1,15 @@
$ javac src/Timeouts.java && java Timeouts (28-beta+21-ea)
-- inside the deadline
[fast, fast]
-- deadline 200 ms, one subtask needs 5 s
ExecutionException, cause = java.util.concurrent.StructuredTaskScope$CancelledByTimeoutException
returned within well under a second (not 5 s): the slow subtask was interrupted
-- the same deadline with a joiner whose timeout() returns a partial result
join() = [fast]
-- a thread factory decides the subtask thread's name
[order-0, order-1]
$ javac src/CollectingJoiner.java && java CollectingJoiner (28-beta+21-ea)
collected (sorted): [1, 3]
-- with a 100 ms timeout and a 5 s subtask
CompletionException, cause = java.util.concurrent.StructuredTaskScope$CancelledByTimeoutException
@@ -0,0 +1,12 @@
$ javac src/ThreadDump.java && java ThreadDump (28-beta+21-ea)
"container": "orders\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@<hash>",
"parent": "<root>",
"owner": <n>,
"tid": <n>,
"virtual": true,
"state": "TIMED_WAITING",
"ThreadDump.lambda$main$6(ThreadDump.java:28)",
"tid": <n>,
"virtual": true,
"state": "TIMED_WAITING",
"ThreadDump.lambda$main$6(ThreadDump.java:28)",
@@ -0,0 +1,4 @@
$ javac src/ScopedValues.java && java ScopedValues (28-beta+21-ea)
forked subtask sees req-7731
plain thread: bound? false
outside the binding: bound? false
@@ -0,0 +1,48 @@
# javap java.util.concurrent.StructuredTaskScope and its nested types (28-beta+21-ea)
Compiled from "StructuredTaskScope.java"
public interface StructuredTaskScope<T, R, R_X extends Throwable> extends AutoCloseable {
public static <T, R, R_X extends Throwable> StructuredTaskScope<T, R, R_X> open(StructuredTaskScope$Joiner<? super T, ? extends R, R_X>, UnaryOperator<StructuredTaskScope$Configuration>);
public static <T, R, R_X extends Throwable> StructuredTaskScope<T, R, R_X> open(StructuredTaskScope$Joiner<? super T, ? extends R, R_X>);
public static <T> StructuredTaskScope<T, Void, ExecutionException> open(UnaryOperator<StructuredTaskScope$Configuration>);
public static <T> StructuredTaskScope<T, Void, ExecutionException> open();
public abstract <U extends T> StructuredTaskScope$Subtask<U> fork(Callable<? extends U>);
public abstract <U extends T> StructuredTaskScope$Subtask<U> fork(Runnable);
public abstract R join() throws R_X, InterruptedException;
public abstract boolean isCancelled();
public abstract void close();
}
Compiled from "StructuredTaskScope.java"
public interface StructuredTaskScope$Joiner<T, R, R_X extends Throwable> {
public default boolean onFork(StructuredTaskScope$Subtask<T>);
public default boolean onComplete(StructuredTaskScope$Subtask<T>);
public abstract R result() throws R_X;
public abstract R timeout() throws R_X;
public static <T, R_X extends Throwable> StructuredTaskScope$Joiner<T, List<T>, R_X> allSuccessfulOrThrow(Function<Throwable, R_X>);
public static <T> StructuredTaskScope$Joiner<T, List<T>, ExecutionException> allSuccessfulOrThrow();
public static <T, R_X extends Throwable> StructuredTaskScope$Joiner<T, T, R_X> anySuccessfulOrThrow(Function<Throwable, R_X>);
public static <T> StructuredTaskScope$Joiner<T, T, ExecutionException> anySuccessfulOrThrow();
public static <T, R_X extends Throwable> StructuredTaskScope$Joiner<T, Void, R_X> awaitAllSuccessfulOrThrow(Function<Throwable, R_X>);
public static <T> StructuredTaskScope$Joiner<T, Void, ExecutionException> awaitAllSuccessfulOrThrow();
public static <T> StructuredTaskScope$Joiner<T, List<StructuredTaskScope$Subtask<T>>, RuntimeException> allUntil(Predicate<? super StructuredTaskScope$Subtask<T>>);
}
Compiled from "StructuredTaskScope.java"
public interface StructuredTaskScope$Configuration {
public abstract StructuredTaskScope$Configuration withThreadFactory(ThreadFactory);
public abstract StructuredTaskScope$Configuration withName(String);
public abstract StructuredTaskScope$Configuration withTimeout(java.time.Duration);
}
Compiled from "StructuredTaskScope.java"
public interface StructuredTaskScope$Subtask<T> extends Supplier<T> {
public abstract StructuredTaskScope$Subtask$State state();
public abstract T get();
public abstract Throwable exception();
}
Compiled from "StructuredTaskScope.java"
public final class StructuredTaskScope$Subtask$State extends Enum<StructuredTaskScope$Subtask$State> {
public static final StructuredTaskScope$Subtask$State UNAVAILABLE;
public static final StructuredTaskScope$Subtask$State SUCCESS;
public static final StructuredTaskScope$Subtask$State FAILED;
public static StructuredTaskScope$Subtask$State[] values();
public static StructuredTaskScope$Subtask$State valueOf(String);
static {};
}
@@ -0,0 +1,74 @@
$ diff api-25 api-26 (signature lines only; the first line of each file names the build)
< public static <T, R> StructuredTaskScope<T, R> open(StructuredTaskScope$Joiner<? super T, ? extends R>, Function<StructuredTaskScope$Configuration, StructuredTaskScope$Configuration>);
> public static <T, R> StructuredTaskScope<T, R> open(StructuredTaskScope$Joiner<? super T, ? extends R>, UnaryOperator<StructuredTaskScope$Configuration>);
< public default boolean onFork(StructuredTaskScope$Subtask<? extends T>);
< public default boolean onComplete(StructuredTaskScope$Subtask<? extends T>);
> public default boolean onFork(StructuredTaskScope$Subtask<T>);
> public default boolean onComplete(StructuredTaskScope$Subtask<T>);
> public default void onTimeout();
< public static <T> StructuredTaskScope$Joiner<T, stream.Stream<StructuredTaskScope$Subtask<T>>> allSuccessfulOrThrow();
< public static <T> StructuredTaskScope$Joiner<T, T> anySuccessfulResultOrThrow();
> public static <T> StructuredTaskScope$Joiner<T, List<T>> allSuccessfulOrThrow();
> public static <T> StructuredTaskScope$Joiner<T, T> anySuccessfulOrThrow();
< public static <T> StructuredTaskScope$Joiner<T, stream.Stream<StructuredTaskScope$Subtask<T>>> allUntil(Predicate<StructuredTaskScope$Subtask<? extends T>>);
> public static <T> StructuredTaskScope$Joiner<T, List<StructuredTaskScope$Subtask<T>>> allUntil(Predicate<StructuredTaskScope$Subtask<T>>);
$ diff api-26 api-27 (signature lines only; the first line of each file names the build)
< public interface StructuredTaskScope<T, R> extends AutoCloseable {
< public static <T, R> StructuredTaskScope<T, R> open(StructuredTaskScope$Joiner<? super T, ? extends R>, UnaryOperator<StructuredTaskScope$Configuration>);
< public static <T, R> StructuredTaskScope<T, R> open(StructuredTaskScope$Joiner<? super T, ? extends R>);
< public static <T> StructuredTaskScope<T, Void> open();
> public interface StructuredTaskScope<T, R, R_X extends Throwable> extends AutoCloseable {
> public static <T, R, R_X extends Throwable> StructuredTaskScope<T, R, R_X> open(StructuredTaskScope$Joiner<? super T, ? extends R, R_X>, UnaryOperator<StructuredTaskScope$Configuration>);
> public static <T, R, R_X extends Throwable> StructuredTaskScope<T, R, R_X> open(StructuredTaskScope$Joiner<? super T, ? extends R, R_X>);
> public static <T> StructuredTaskScope<T, Void, ExecutionException> open(UnaryOperator<StructuredTaskScope$Configuration>);
> public static <T> StructuredTaskScope<T, Void, ExecutionException> open();
< public abstract R join() throws InterruptedException;
> public abstract R join() throws R_X, InterruptedException;
< public interface StructuredTaskScope$Joiner<T, R> {
> public interface StructuredTaskScope$Joiner<T, R, R_X extends Throwable> {
< public default void onTimeout();
< public abstract R result() throws Throwable;
< public static <T> StructuredTaskScope$Joiner<T, List<T>> allSuccessfulOrThrow();
< public static <T> StructuredTaskScope$Joiner<T, T> anySuccessfulOrThrow();
< public static <T> StructuredTaskScope$Joiner<T, Void> awaitAllSuccessfulOrThrow();
< public static <T> StructuredTaskScope$Joiner<T, Void> awaitAll();
< public static <T> StructuredTaskScope$Joiner<T, List<StructuredTaskScope$Subtask<T>>> allUntil(Predicate<StructuredTaskScope$Subtask<T>>);
> public abstract R result() throws R_X;
> public abstract R timeout() throws R_X;
> public static <T, R_X extends Throwable> StructuredTaskScope$Joiner<T, List<T>, R_X> allSuccessfulOrThrow(Function<Throwable, R_X>);
> public static <T> StructuredTaskScope$Joiner<T, List<T>, ExecutionException> allSuccessfulOrThrow();
> public static <T, R_X extends Throwable> StructuredTaskScope$Joiner<T, T, R_X> anySuccessfulOrThrow(Function<Throwable, R_X>);
> public static <T> StructuredTaskScope$Joiner<T, T, ExecutionException> anySuccessfulOrThrow();
> public static <T, R_X extends Throwable> StructuredTaskScope$Joiner<T, Void, R_X> awaitAllSuccessfulOrThrow(Function<Throwable, R_X>);
> public static <T> StructuredTaskScope$Joiner<T, Void, ExecutionException> awaitAllSuccessfulOrThrow();
> public static <T> StructuredTaskScope$Joiner<T, List<StructuredTaskScope$Subtask<T>>, RuntimeException> allUntil(Predicate<? super StructuredTaskScope$Subtask<T>>);
$ diff api-27 api-28 (signature lines only; the first line of each file names the build)
(no difference)
$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (25.0.5+8-LTS)
java/util/concurrent/StructuredTaskScope$FailedException.class
java/util/concurrent/StructuredTaskScope$TimeoutException.class
$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (26.0.2.1+1)
java/util/concurrent/StructuredTaskScope$FailedException.class
java/util/concurrent/StructuredTaskScope$TimeoutException.class
$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (27.0.1.0.2+1)
java/util/concurrent/StructuredTaskScope$CancelledByTimeoutException.class
$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' (28-beta+21-ea)
java/util/concurrent/StructuredTaskScope$CancelledByTimeoutException.class
$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 (26.0.2.1+1)
jdk.internal.javac.PreviewFeature(
feature=Ljdk/internal/javac/PreviewFeature$Feature;.STRUCTURED_CONCURRENCY
$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 (27.0.1.0.2+1)
jdk.internal.javac.PreviewFeature(
feature=Ljdk/internal/javac/PreviewFeature$Feature;.STRUCTURED_CONCURRENCY
$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 (28-beta+21-ea)
(no @PreviewFeature annotation: the API is final)
@@ -0,0 +1,83 @@
$ javac --enable-preview --release 26 broken/MissingCatch.java (26.0.2.1+1)
exit=0
$ javac broken/MissingCatch.java (28-beta+21-ea)
broken/MissingCatch.java:8: error: unreported exception ExecutionException; must be caught or declared to be thrown
scope.join();
^
1 error
exit=1
$ javac --enable-preview --release 26 broken/OldFailedException.java (26.0.2.1+1)
exit=0
$ javac broken/OldFailedException.java (28-beta+21-ea)
broken/OldFailedException.java:9: error: cannot find symbol
} catch (StructuredTaskScope.FailedException e) {
^
symbol: class FailedException
location: interface StructuredTaskScope
1 error
exit=1
$ javac --enable-preview --release 26 broken/OldTimeoutException.java (26.0.2.1+1)
exit=0
$ javac broken/OldTimeoutException.java (28-beta+21-ea)
broken/OldTimeoutException.java:11: error: cannot find symbol
} catch (StructuredTaskScope.TimeoutException e) {
^
symbol: class TimeoutException
location: interface StructuredTaskScope
1 error
exit=1
$ javac --enable-preview --release 26 broken/AwaitAll.java (26.0.2.1+1)
exit=0
$ javac broken/AwaitAll.java (28-beta+21-ea)
broken/AwaitAll.java:7: error: cannot find symbol
try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll())) {
^
symbol: method <String>awaitAll()
location: interface Joiner
1 error
exit=1
$ javac --enable-preview --release 26 broken/OldJoiner.java (26.0.2.1+1)
exit=0
$ javac broken/OldJoiner.java (28-beta+21-ea)
broken/OldJoiner.java:7: error: wrong number of type arguments; required 3
public class OldJoiner<T> implements Joiner<T, List<T>> {
^
1 error
exit=1
$ javac --enable-preview --release 26 broken/ShutdownOnFailure.java (26.0.2.1+1)
broken/ShutdownOnFailure.java:6: error: cannot find symbol
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
^
symbol: class ShutdownOnFailure
location: interface StructuredTaskScope
1 error
exit=1
$ javac broken/ShutdownOnFailure.java (28-beta+21-ea)
broken/ShutdownOnFailure.java:6: error: cannot find symbol
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
^
symbol: class ShutdownOnFailure
location: interface StructuredTaskScope
1 error
exit=1
# the same sources on JDK 25 for ShutdownOnFailure
$ javac --enable-preview --release 25 broken/ShutdownOnFailure.java (25.0.5+8-LTS)
broken/ShutdownOnFailure.java:6: error: cannot find symbol
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
^
symbol: class ShutdownOnFailure
location: interface StructuredTaskScope
1 error
exit=1
@@ -0,0 +1,59 @@
# compile on 27 with preview, then run the class file on 27 and on 28
$ java --enable-preview Server (27.0.1.0.2+1)
Response[user=Alice, order=42]
Runtime: 27.0.1.0.2+1
$ java --enable-preview Server (28-beta+21-ea)
Error: LinkageError occurred while loading main class Server
java.lang.UnsupportedClassVersionError: Server (class file version 71.65535) was compiled with preview features that are unsupported. This version of the Java Runtime only recognizes preview features for class file version 72.65535
$ java Server (27.0.1.0.2+1)
Error: LinkageError occurred while loading main class Server
java.lang.UnsupportedClassVersionError: Preview features are not enabled for Server (class file version 71.65535). Try running with '--enable-preview'
# the class file's minor version
minor version: 65535
major version: 71
# javac 28 asked for --release 27
$ javac --release 27 src/Server.java (28-beta+21-ea)
src/Server.java:2: error: StructuredTaskScope is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope;
^
(use --enable-preview to enable preview APIs)
src/Server.java:3: error: StructuredTaskScope is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope.Subtask;
^
(use --enable-preview to enable preview APIs)
src/Server.java:3: error: Subtask is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope.Subtask;
^
(use --enable-preview to enable preview APIs)
src/Server.java:22: error: StructuredTaskScope is a preview API and is disabled by default.
try (var scope = StructuredTaskScope.open()) { // 1. open
^
(use --enable-preview to enable preview APIs)
src/Server.java:23: error: Subtask is a preview API and is disabled by default.
Subtask<String> user = scope.fork(this::findUser); // 2. fork
^
(use --enable-preview to enable preview APIs)
src/Server.java:24: error: Subtask is a preview API and is disabled by default.
Subtask<Integer> order = scope.fork(this::fetchOrder); // fork
^
(use --enable-preview to enable preview APIs)
6 errors
# javac 28 with --enable-preview --release 28 on the same file (nothing in it needs preview)
$ javac --enable-preview --release 28 src/Server.java (28-beta+21-ea)
Note: src/Server.java uses preview features of Java SE 28.
Note: Recompile with -Xlint:preview for details.
exit=0
# where that Note comes from: the first lines of -Xlint:preview
$ javac --enable-preview --release 28 -Xlint:preview src/Server.java 2>&1 | head -3 (28-beta+21-ea)
warning: [preview] class file for /@/java.base/java/lang/Record.sig uses preview features of Java SE 28.
warning: [preview] class file for /@/java.base/java/time/Duration.sig uses preview features of Java SE 28.
warning: [preview] class file for /@/java.base/java/lang/Integer.sig uses preview features of Java SE 28.
# recompile on 28 without any flag and run on 28
$ javac src/Server.java && java Server (28-beta+21-ea)
Response[user=Alice, order=42]
Runtime: 28-beta+21-ea
@@ -0,0 +1,9 @@
$ javac src/Snippets.java && java Snippets (28-beta+21-ea)
Response[user=Alice, order=42]
Response[user=Alice, order=42]
[1, 2]
a
Response[user=Alice, order=42]
$ javac src/QuickReference.java && java QuickReference (28-beta+21-ea)
quick reference compiled and ran
@@ -0,0 +1,20 @@
$ java RandomTasks 5 (28-beta+21-ea)
Duration: 487
Duration: 92
Duration: 474
Duration: 424
Duration: 506
Result: 487
Result: 92
Result: 474
Result: 424
Result: 506
$ java RandomTasks 3 (28-beta+21-ea)
Duration: 734
Duration: 660
Duration: 210
Duration: 581
Duration: 128
ExecutionException: TooSlowException: Duration 734 greater than threshold 700
@@ -0,0 +1,3 @@
$ javac src/StuckClose.java && java StuckClose (28-beta+21-ea)
subtask that responds to interrupts: caught fails at 50 ms; the try block took >= 1400 ms: false
subtask that ignores interrupts : caught fails at 50 ms; the try block took >= 1400 ms: true
@@ -0,0 +1,23 @@
$ jcmd <pid> Thread.dump_to_file -format=json threads.json (28-beta+21-ea)
<pid>:
Created <tmp>/threads.json
# the scope "orders" and its child scope in threads.json (stack frames, timestamps and ids trimmed or masked)
"container": "orders\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@<hash>",
"parent": "<root>",
"owner": <n>,
"tid": <n>,
"virtual": true,
"state": "TIMED_WAITING",
"ThreadDump.lambda$main$1(ThreadDump.java:12)",
"tid": <n>,
"virtual": true,
"state": "WAITING",
"ThreadDump.lambda$main$2(ThreadDump.java:16)",
"container": "inventory\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@<hash>",
"parent": "orders\/jdk.internal.misc.ThreadFlock$ThreadContainerImpl@<hash>",
"owner": <n>,
"tid": <n>,
"virtual": true,
"state": "TIMED_WAITING",
"ThreadDump.lambda$main$4(ThreadDump.java:15)",
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# Structured concurrency, final in JDK 28 (JEP 543). Regenerates every file in output/.
# JDK25=/path JDK26=/path JDK27=/path JDK28=/path ./run.sh
# JDK 28 is an early-access build at the time of writing (GA proposed 2027-03-09); JDK 27 still treats the API as a preview.
set -uo pipefail
unset JAVA_TOOL_OPTIONS
HERE="$(cd "$(dirname "$0")" && pwd)"
JDK25="${JDK25:-/opt/jdks/jdk25}"; JDK26="${JDK26:-/opt/jdks/jdk26}"; JDK27="${JDK27:-/opt/jdks/jdk27}"; JDK28="${JDK28:-/opt/jdks/jdk28}"
OUT="$HERE/output"; mkdir -p "$OUT"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT
cd "$HERE"
rel() { case "$1" in "$JDK25") echo 25;; "$JDK26") echo 26;; "$JDK27") echo 27;; *) echo 28;; esac; }
t() { "$1/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'; }
mk() { mktemp -d -p "$B"; }
# plain compile + run (no flags)
cr() { local jdk="$1" src="$2" cls="$3" o; o="$(mk)"
echo "\$ javac $src && java $cls ($(t "$jdk"))"
"$jdk/bin/javac" --add-modules jdk.management -d "$o" "$src" 2>&1 && "$jdk/bin/java" --add-modules jdk.management -cp "$o" "$cls" 2>&1; }
# preview compile + run
crp() { local jdk="$1" src="$2" cls="$3" o r; o="$(mk)"; r="$(rel "$jdk")"
echo "\$ javac --enable-preview --release $r $src && java --enable-preview $cls ($(t "$jdk"))"
"$jdk/bin/javac" --enable-preview --release "$r" --add-modules jdk.management -d "$o" "$src" 2>&1 | grep -v '^Note:'
"$jdk/bin/java" --enable-preview --add-modules jdk.management -cp "$o" "$cls" 2>&1; }
# compile only, no flags (prints the compiler's own message and the exit status)
co() { local jdk="$1" src="$2" o; o="$(mk)"
echo "\$ javac $src ($(t "$jdk"))"
"$jdk/bin/javac" -d "$o" "$src" 2>&1 | grep -v '^Note:'; echo "exit=${PIPESTATUS[0]}"; }
# compile only, with --enable-preview for the JDK's own release
cop() { local jdk="$1" src="$2" o r; o="$(mk)"; r="$(rel "$jdk")"
echo "\$ javac --enable-preview --release $r $src ($(t "$jdk"))"
"$jdk/bin/javac" --enable-preview --release "$r" -d "$o" "$src" 2>&1 | grep -v '^Note:'; echo "exit=${PIPESTATUS[0]}"; }
# 01 - the canonical program: final on 28 (no flags), preview on 27 (flags), and without the flag on 27
{ cr "$JDK28" src/Server.java Server; echo; crp "$JDK27" src/Server.java Server; echo; co "$JDK27" src/Server.java; } > "$OUT/01-server.txt"
# 02 - the failure that motivates the feature, then the same subtasks in a scope
{ cr "$JDK28" src/Unstructured.java Unstructured; echo; cr "$JDK28" src/FailFast.java FailFast; } > "$OUT/02-unstructured-vs-structured.txt"
# 03 - the built-in joiners
{ cr "$JDK28" src/Joiners.java Joiners; } > "$OUT/03-joiners.txt"
# 04 - structure violations and failure handling
{ cr "$JDK28" src/Failures.java Failures; } > "$OUT/04-failures.txt"
# 05 - timeouts and a custom joiner
{ cr "$JDK28" src/Timeouts.java Timeouts; echo; cr "$JDK28" src/CollectingJoiner.java CollectingJoiner; } > "$OUT/05-timeouts-and-custom-joiner.txt"
# 06 - the thread dump
{ cr "$JDK28" src/ThreadDump.java ThreadDump; } > "$OUT/06-thread-dump.txt"
# 07 - scoped values
{ cr "$JDK28" src/ScopedValues.java ScopedValues; } > "$OUT/07-scoped-values.txt"
# 08 - the API surface, release by release (javap of java.base)
api() { local jdk="$1"; echo "# javap java.util.concurrent.StructuredTaskScope and its nested types ($(t "$jdk"))"
for c in 'StructuredTaskScope' 'StructuredTaskScope$Joiner' 'StructuredTaskScope$Configuration' 'StructuredTaskScope$Subtask' 'StructuredTaskScope$Subtask$State'; do
"$jdk/bin/javap" --module java.base "java.util.concurrent.$c" 2>&1; done |
sed 's/java\.util\.concurrent\.//g;s/java\.lang\.//g;s/java\.util\.function\.//g;s/java\.util\.//g'; }
{ api "$JDK28"; } > "$OUT/08-api-28.txt"
for v in 25 26 27 28; do eval "j=\$JDK$v"; api "$j" > "$B/api$v.txt"; done
{ for p in "25 26" "26 27" "27 28"; do set -- $p
echo "\$ diff api-$1 api-$2 (signature lines only; the first line of each file names the build)"
if diff <(tail -n +2 "$B/api$1.txt") <(tail -n +2 "$B/api$2.txt") > "$B/d.txt"; then echo "(no difference)"
else grep -v '^[0-9,]*[acd][0-9,]*$' "$B/d.txt" | grep -v '^---$'; fi; echo; done
for v in 25 26 27 28; do eval "j=\$JDK$v"; echo "\$ jimage list modules | grep -E 'FailedException|TimeoutException|CancelledByTimeout' ($(t "$j"))"
"$j/bin/jimage" list "$j/lib/modules" 2>/dev/null | grep -E 'StructuredTaskScope\$(FailedException|TimeoutException|CancelledByTimeoutException)' | sed 's/^ *//'; echo; done
for v in 26 27 28; do eval "j=\$JDK$v"; echo "\$ javap -v StructuredTaskScope | grep 'PreviewFeature(' -A1 ($(t "$j"))"
"$j/bin/javap" -v --module java.base java.util.concurrent.StructuredTaskScope 2>&1 | grep -A1 'jdk.internal.javac.PreviewFeature(' | head -2 || true
"$j/bin/javap" -v --module java.base java.util.concurrent.StructuredTaskScope 2>&1 | grep -q 'PreviewFeature(' || echo "(no @PreviewFeature annotation: the API is final)"; echo; done
} > "$OUT/09-api-diff.txt"
# 10 - code written for 25 or 26 against 27 and 28 (the compiler's own words)
{ for f in MissingCatch OldFailedException OldTimeoutException AwaitAll OldJoiner ShutdownOnFailure; do
for j in "$JDK26" "$JDK28"; do
if [ "$j" = "$JDK26" ]; then cop "$j" broken/$f.java; else co "$j" broken/$f.java; fi; echo; done; done
echo "# the same sources on JDK 25 for ShutdownOnFailure"; cop "$JDK25" broken/ShutdownOnFailure.java; } > "$OUT/10-broken-old-api.txt"
# 11 - the preview-flag traps
{ O="$(mk)"
echo "# compile on 27 with preview, then run the class file on 27 and on 28"
"$JDK27/bin/javac" --enable-preview --release 27 -d "$O" src/Server.java 2>&1 | grep -v '^Note:'
echo "\$ java --enable-preview Server ($(t "$JDK27"))"; "$JDK27/bin/java" --enable-preview -cp "$O" Server 2>&1
echo "\$ java --enable-preview Server ($(t "$JDK28"))"; "$JDK28/bin/java" --enable-preview -cp "$O" Server 2>&1
echo "\$ java Server ($(t "$JDK27"))"; "$JDK27/bin/java" -cp "$O" Server 2>&1
echo; echo "# the class file's minor version"
"$JDK28/bin/javap" -v -cp "$O" Server 2>&1 | grep -E 'minor version|major version'
echo; echo "# javac 28 asked for --release 27"
echo "\$ javac --release 27 src/Server.java ($(t "$JDK28"))"; "$JDK28/bin/javac" --release 27 -d "$(mk)" src/Server.java 2>&1 | grep -v '^Note:'
echo; echo "# javac 28 with --enable-preview --release 28 on the same file (nothing in it needs preview)"
echo "\$ javac --enable-preview --release 28 src/Server.java ($(t "$JDK28"))"; "$JDK28/bin/javac" --enable-preview --release 28 -d "$(mk)" src/Server.java 2>&1; echo "exit=$?"
echo; echo "# where that Note comes from: the first lines of -Xlint:preview"
echo "\$ javac --enable-preview --release 28 -Xlint:preview src/Server.java 2>&1 | head -3 ($(t "$JDK28"))"
"$JDK28/bin/javac" --enable-preview --release 28 -Xlint:preview -d "$(mk)" src/Server.java 2>&1 | head -3
echo; echo "# recompile on 28 without any flag and run on 28"
cr "$JDK28" src/Server.java Server
} > "$OUT/11-preview-traps.txt"
# 12 - the short fragments quoted in the article compile and run on 28
{ cr "$JDK28" src/Snippets.java Snippets; echo; cr "$JDK28" src/QuickReference.java QuickReference; } > "$OUT/12-snippets.txt"
# 13 - five seeded random tasks: one run where all succeed, one where a task is too slow
{ o="$(mk)"; "$JDK28/bin/javac" -d "$o" src/RandomTasks.java 2>&1
for seed in 5 3; do echo "\$ java RandomTasks $seed ($(t "$JDK28"))"; "$JDK28/bin/java" -cp "$o" RandomTasks $seed 2>&1; echo; done; } > "$OUT/13-random-tasks.txt"
# 14 - a subtask that ignores interrupts keeps close() waiting
{ cr "$JDK28" src/StuckClose.java StuckClose; } > "$OUT/14-stuck-close.txt"
# 15 - the thread dump taken from outside with jcmd, as the JEP describes it
{ o="$(mk)"; "$JDK28/bin/javac" --add-modules jdk.management -d "$o" src/ThreadDump.java 2>&1
"$JDK28/bin/java" -cp "$o" ThreadDump hold > /dev/null 2>&1 &
pid=$!; sleep 2
echo "\$ jcmd <pid> Thread.dump_to_file -format=json threads.json ($(t "$JDK28"))"
"$JDK28/bin/jcmd" "$pid" Thread.dump_to_file -format=json "$B/threads.json" 2>&1 | sed -E 's#'"$B"'#<tmp>#; s/^[0-9]+:$/<pid>:/'
kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
echo; echo "# the scope \"orders\" and its child scope in threads.json (stack frames, timestamps and ids trimmed or masked)"
awk '/"container": "orders/{f=1} f' "$B/threads.json" |
sed -E 's/@[0-9a-f]+/@<hash>/; s/"(tid|owner)": [0-9]+/"\1": <n>/' |
grep -E '"(container|parent|owner|tid|virtual|state)"|ThreadDump.lambda' | sed -E 's/^ +//'
} > "$OUT/15-jcmd-thread-dump.txt"
echo done
@@ -0,0 +1,54 @@
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** A custom completion policy: keep what succeeds, ignore what fails. The final Joiner has THREE type parameters. */
public class CollectingJoiner<T> implements Joiner<T, List<T>, CompletionException> {
private final Queue<T> results = new ConcurrentLinkedQueue<>();
@Override
public boolean onComplete(Subtask<T> subtask) {
if (subtask.state() == Subtask.State.SUCCESS) {
results.add(subtask.get());
}
return false; // never cancel early: let everyone finish
}
@Override
public List<T> result() {
return List.copyOf(results);
}
@Override
public List<T> timeout() {
// the scope was opened with a timeout and it expired
throw new CompletionException(new StructuredTaskScope.CancelledByTimeoutException());
}
static <T> List<T> allSuccessful(List<Callable<T>> tasks) throws InterruptedException {
try (var scope = StructuredTaskScope.open(new CollectingJoiner<T>())) {
tasks.forEach(scope::fork);
return scope.join();
}
}
public static void main(String[] args) throws Exception {
List<Callable<Integer>> tasks = List.of(
() -> 1,
() -> { throw new IllegalStateException("ignored"); },
() -> 3);
List<Integer> r = new ArrayList<>(allSuccessful(tasks));
Collections.sort(r);
System.out.println("collected (sorted): " + r);
System.out.println("-- with a 100 ms timeout and a 5 s subtask");
try (var scope = StructuredTaskScope.open(new CollectingJoiner<Integer>(),
cf -> cf.withTimeout(java.time.Duration.ofMillis(100)))) {
scope.fork(() -> { Thread.sleep(5000); return 1; });
scope.join();
} catch (CompletionException e) {
System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause().getClass().getName());
}
}
}
@@ -0,0 +1,41 @@
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** The same two subtasks in a StructuredTaskScope: the failure of one cancels the other, before handle() returns. */
public class FailFast {
static String findUser() throws InterruptedException {
Thread.sleep(50);
throw new IllegalStateException("user service down");
}
static int fetchOrder() throws InterruptedException {
try {
Thread.sleep(300);
System.out.println(" fetchOrder() finished");
return 42;
} catch (InterruptedException e) {
System.out.println(" fetchOrder() was interrupted");
throw e;
}
}
static String handle() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(FailFast::findUser);
Subtask<Integer> order = scope.fork(FailFast::fetchOrder);
scope.join();
return user.get() + " " + order.get();
} catch (ExecutionException e) {
System.out.println("handle() failed: " + e.getCause());
return null;
}
}
public static void main(String[] args) throws Exception {
handle();
System.out.println("handle() has returned. Waiting 400 ms to see what the other thread does:");
Thread.sleep(400);
System.out.println("(nothing more happened)");
}
}
@@ -0,0 +1,87 @@
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** What goes wrong when the structure is violated, and what the exception looks like. */
public class Failures {
static void attempt(String title, Callable<Object> body) {
System.out.println("-- " + title);
try {
Object r = body.call();
System.out.println(" returned " + r);
} catch (Throwable t) {
System.out.println(" " + t.getClass().getName() + ": " + t.getMessage());
}
}
public static void main(String[] args) throws Exception {
attempt("Subtask.get() before join()", () -> {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> s = scope.fork(() -> "x");
return s.get();
}
});
attempt("leaving the try block without join()", () -> {
try (var scope = StructuredTaskScope.open()) {
scope.fork(() -> "x");
return "left the block";
}
});
attempt("fork() from a thread that does not own the scope", () -> {
try (var scope = StructuredTaskScope.open()) {
var other = new CompletableFuture<Object>();
Thread.ofVirtual().start(() -> {
try { scope.fork(() -> "x"); other.complete("forked"); }
catch (Throwable t) { other.complete(t.getClass().getName() + ": " + t.getMessage()); }
});
Object r = other.get();
scope.join();
return r;
}
});
attempt("closing scopes out of order (not try-with-resources)", () -> {
var outer = StructuredTaskScope.open();
var inner = StructuredTaskScope.open();
try {
outer.close(); // the inner scope is still open
return "closed outer first";
} finally {
try { inner.close(); } catch (Throwable ignore) { }
try { outer.close(); } catch (Throwable ignore) { }
}
});
attempt("join() twice", () -> {
try (var scope = StructuredTaskScope.open()) {
scope.fork(() -> "x");
scope.join();
return scope.join();
}
});
System.out.println("-- the failure surfaces as ExecutionException; pattern-match the cause");
try (var scope = StructuredTaskScope.open()) {
scope.fork(() -> { throw new IOException("disk gone"); });
scope.join();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
switch (cause) {
case IOException ioe -> System.out.println(" I/O failed: " + ioe.getMessage());
default -> System.out.println(" unexpected: " + cause);
}
}
System.out.println("-- two subtasks fail: which cause do you get?");
try (var scope = StructuredTaskScope.open()) {
scope.fork(() -> { Thread.sleep(20); throw new IllegalStateException("second"); });
scope.fork(() -> { throw new IllegalStateException("first"); });
scope.join();
} catch (ExecutionException e) {
System.out.println(" cause = " + e.getCause().getMessage() + " (the first to fail wins)");
}
}
}
@@ -0,0 +1,89 @@
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
import java.util.concurrent.atomic.AtomicInteger;
/** The built-in Joiner factories, each with a result and a failure. */
public class Joiners {
/** Names of the subtasks that were interrupted; printed after the scope closes so the order is stable. */
static final Set<String> interrupted = new ConcurrentSkipListSet<>();
static void reportInterrupted() {
System.out.println(" interrupted: " + interrupted);
interrupted.clear();
}
static Callable<String> task(String name, long ms) {
return () -> {
try { Thread.sleep(ms); return name; }
catch (InterruptedException e) { interrupted.add(name); throw e; }
};
}
static Callable<String> failing(String name, long ms) {
return () -> { Thread.sleep(ms); throw new IllegalStateException(name + " failed"); };
}
/** A checked exception of our own, to show the Function overloads. */
static class ServiceException extends Exception {
ServiceException(Throwable cause) { super("service call failed: " + cause.getMessage(), cause); }
}
public static void main(String[] args) throws Exception {
System.out.println("-- allSuccessfulOrThrow(): List in FORK order, not completion order");
try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow())) {
scope.fork(task("slow", 150)); scope.fork(task("fast", 10)); scope.fork(task("medium", 80));
System.out.println(" " + scope.join());
}
System.out.println("-- allSuccessfulOrThrow(): one failure cancels the others, join() throws ExecutionException");
try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow())) {
scope.fork(task("slow", 500)); scope.fork(failing("broken", 30));
scope.join();
} catch (ExecutionException e) {
System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause());
}
reportInterrupted();
System.out.println("-- anySuccessfulOrThrow(): the race; losers are interrupted");
try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulOrThrow())) {
scope.fork(task("A-300ms", 300)); scope.fork(task("B-50ms", 50)); scope.fork(task("C-300ms", 300));
System.out.println(" winner: " + scope.join());
}
reportInterrupted();
System.out.println("-- anySuccessfulOrThrow(): when every subtask fails");
try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulOrThrow())) {
scope.fork(failing("one", 10)); scope.fork(failing("two", 20));
scope.join();
} catch (ExecutionException e) {
System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause());
}
System.out.println("-- awaitAllSuccessfulOrThrow(): returns null (Void), like the zero-argument open()");
try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow())) {
Subtask<String> a = scope.fork(task("a", 10));
Subtask<String> b = scope.fork(task("b", 20));
Void v = scope.join();
System.out.println(" join() = " + v + ", a=" + a.get() + ", b=" + b.get());
}
System.out.println("-- allUntil(predicate): stop when two subtasks have completed");
var done = new AtomicInteger();
try (var scope = StructuredTaskScope.open(Joiner.<String>allUntil(s -> done.incrementAndGet() == 2))) {
scope.fork(task("t1", 20)); scope.fork(task("t2", 60)); scope.fork(task("t3", 400)); scope.fork(task("t4", 400));
List<Subtask<String>> subtasks = scope.join();
subtasks.forEach(s -> System.out.println(" " + s.state() + (s.state() == Subtask.State.SUCCESS ? " " + s.get() : "")));
}
reportInterrupted();
System.out.println("-- the Function overload: join() throws OUR exception type");
try (var scope = StructuredTaskScope.open(Joiner.<String, ServiceException>allSuccessfulOrThrow(ServiceException::new))) {
scope.fork(failing("orders", 10));
scope.join();
} catch (ServiceException e) {
System.out.println(" caught " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
@@ -0,0 +1,40 @@
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** One line per API in the article's quick reference. Compiles on JDK 28 with no flags. */
public class QuickReference {
@SuppressWarnings("unused")
static void shapes(ThreadFactory factory) throws Exception {
// region quickref
// Open a scope: the policy is the Joiner, the type parameters are <T subtask result, R join() result, R_X join() exception>
var plain = StructuredTaskScope.<String>open(); // join() -> Void, throws ExecutionException
var all = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow()); // join() -> List<String>, in fork order
var any = StructuredTaskScope.open(Joiner.<String>anySuccessfulOrThrow()); // join() -> String, first success wins
var await = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow()); // join() -> Void
var until = StructuredTaskScope.open(Joiner.<String>allUntil(s -> false)); // join() -> List<Subtask<String>>, no checked exception
var mine = StructuredTaskScope.open(Joiner.<String, IllegalStateException>allSuccessfulOrThrow(IllegalStateException::new)); // your exception type
// Configure: name for thread dumps, a deadline for the whole group, a thread factory for the subtasks
var cfg = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
cf -> cf.withName("orders").withTimeout(Duration.ofMillis(500)).withThreadFactory(factory));
// Fork, then read each Subtask only after join()
Subtask<String> subtask = all.fork(() -> "value");
all.join();
String value = subtask.get(); // like Future.resultNow()
Subtask.State state = subtask.state(); // SUCCESS | FAILED | UNAVAILABLE
// Subtask also has exception(); see output/08-api-28.txt for every signature
// endregion quickref
// scopes opened by one thread must be closed in the reverse order (try-with-resources does this for you)
cfg.close(); mine.close(); until.close(); await.close(); any.close(); all.close(); plain.close();
}
public static void main(String[] args) throws Exception {
shapes(Thread.ofVirtual().factory());
System.out.println("quick reference compiled and ran");
}
}
@@ -0,0 +1,33 @@
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
/** Five tasks that sleep a random time and fail if they were "too slow". Seeded, so the transcript is repeatable. */
public class RandomTasks {
static class TooSlowException extends Exception {
TooSlowException(String m) { super(m); }
}
public static void main(String[] args) throws Exception {
long seed = Long.parseLong(args[0]);
int threshold = 700;
Random random = new Random(seed);
List<Callable<Integer>> tasks = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int duration = random.nextInt(1000);
System.out.println("Duration: " + duration);
tasks.add(() -> {
Thread.sleep(duration);
if (duration > threshold) throw new TooSlowException("Duration " + duration + " greater than threshold " + threshold);
return duration;
});
}
try (var scope = StructuredTaskScope.open(Joiner.<Integer>allSuccessfulOrThrow())) {
tasks.forEach(scope::fork);
for (int r : scope.join()) System.out.println("Result: " + r);
} catch (ExecutionException e) {
System.out.println("ExecutionException: " + e.getCause().getClass().getSimpleName() + ": " + e.getCause().getMessage());
}
}
}
@@ -0,0 +1,24 @@
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** Subtasks inherit ScopedValue bindings (JEP 506, final in JDK 25). A plain thread does not. */
public class ScopedValues {
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
public static void main(String[] args) throws Exception {
ScopedValue.where(REQUEST_ID, "req-7731").run(() -> {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> a = scope.fork(() -> "forked subtask sees " + REQUEST_ID.get());
scope.join();
System.out.println(a.get());
} catch (Exception e) {
throw new RuntimeException(e);
}
Thread t = new Thread(() ->
System.out.println("plain thread: bound? " + REQUEST_ID.isBound()));
t.start();
try { t.join(); } catch (InterruptedException e) { }
});
System.out.println("outside the binding: bound? " + REQUEST_ID.isBound());
}
}
@@ -0,0 +1,36 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
record Response(String user, int order) {}
/** The canonical program: open, fork, join, process, close. */
public class Server {
String findUser() throws InterruptedException {
Thread.sleep(120); // pretend: a network call
return "Alice";
}
int fetchOrder() throws InterruptedException {
Thread.sleep(80); // pretend: another network call
return 42;
}
// join() now declares ExecutionException (the scope's third type parameter, R_X) as well as InterruptedException
Response handle() throws ExecutionException, InterruptedException {
try (var scope = StructuredTaskScope.open()) { // 1. open
Subtask<String> user = scope.fork(this::findUser); // 2. fork
Subtask<Integer> order = scope.fork(this::fetchOrder); // fork
scope.join(); // 3. wait for BOTH, propagate any failure
return new Response(user.get(), order.get()); // 4. process results
} // 5. close: cancel + wait (automatic)
}
public static void main(String[] args) throws Exception {
System.out.println(new Server().handle());
System.out.println("Runtime: " + Runtime.version());
}
}
@@ -0,0 +1,86 @@
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** The short fragments quoted in the article, as one compiling file (see output/12-snippets-compile.txt). */
public class Snippets {
record Response(String user, int order) {}
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
String findUser() throws InterruptedException { return "Alice"; }
int fetchOrder() throws InterruptedException { return 42; }
class ExecutorVersion {
// region unstructured
Response handle() throws ExecutionException, InterruptedException {
Future<String> user = executor.submit(() -> findUser());
Future<Integer> order = executor.submit(() -> fetchOrder());
String theUser = user.get(); // join findUser
int theOrder = order.get(); // join fetchOrder
return new Response(theUser, theOrder);
}
// endregion unstructured
}
class SequentialVersion {
// region sequential
Response handle() throws IOException, InterruptedException {
String theUser = findUser();
int theOrder = fetchOrder();
return new Response(theUser, theOrder);
}
// endregion sequential
}
// region runConcurrently
<T> List<T> runConcurrently(Collection<Callable<T>> tasks) throws ExecutionException, InterruptedException {
try (var scope = StructuredTaskScope.open(Joiner.<T>allSuccessfulOrThrow())) {
tasks.forEach(scope::fork);
return scope.join(); // List<T> of results; throws ExecutionException if any task fails
}
}
// endregion runConcurrently
// region race
<T> T race(Collection<Callable<T>> tasks) throws ExecutionException, InterruptedException {
try (var scope = StructuredTaskScope.open(Joiner.<T>anySuccessfulOrThrow())) {
tasks.forEach(scope::fork);
return scope.join(); // result of the first subtask to succeed
}
}
// endregion race
// region handleWithCatch
Response handleWithCatch() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(this::findUser);
Subtask<Integer> order = scope.fork(this::fetchOrder);
scope.join();
return new Response(user.get(), order.get());
} catch (ExecutionException e) {
Throwable cause = e.getCause();
switch (cause) {
case IOException ioe -> System.err.println("I/O failed: " + ioe);
default -> System.err.println("Unexpected: " + cause);
}
throw new IllegalStateException(e);
}
}
// endregion handleWithCatch
public static void main(String[] args) throws Exception {
var s = new Snippets();
try {
System.out.println(s.new ExecutorVersion().handle());
System.out.println(s.new SequentialVersion().handle());
System.out.println(s.runConcurrently(List.of(() -> 1, () -> 2)));
String winner = s.race(List.<Callable<String>>of(() -> "a")); System.out.println(winner);
System.out.println(s.handleWithCatch());
} finally {
s.executor.close();
}
}
}
@@ -0,0 +1,33 @@
import java.util.concurrent.*;
/** A subtask that ignores interrupts keeps close() waiting, even though the scope was cancelled at once. */
public class StuckClose {
static int stubborn() {
long end = System.nanoTime() + 1_500_000_000L;
while (System.nanoTime() < end) { /* busy loop: never checks the interrupt flag */ }
return 1;
}
static int cooperative() throws InterruptedException {
Thread.sleep(1_500); // blocking calls like sleep() respond to interrupt
return 1;
}
static void run(String label, Callable<Integer> slow) throws Exception {
long t0 = System.nanoTime();
try (var scope = StructuredTaskScope.open()) {
scope.fork(slow);
scope.fork(() -> { Thread.sleep(50); throw new IllegalStateException("fails at 50 ms"); });
scope.join();
} catch (ExecutionException e) {
long ms = (System.nanoTime() - t0) / 1_000_000;
System.out.println(label + ": caught " + e.getCause().getMessage() + "; the try block took >= 1400 ms: " + (ms >= 1400));
}
}
public static void main(String[] args) throws Exception {
run("subtask that responds to interrupts", StuckClose::cooperative);
run("subtask that ignores interrupts ", StuckClose::stubborn);
}
}
@@ -0,0 +1,53 @@
import com.sun.management.HotSpotDiagnosticMXBean;
import java.lang.management.ManagementFactory;
import java.nio.file.*;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
/** The thread dump shows the scope as a container, its owner, and the threads forked in it. */
public class ThreadDump {
public static void main(String[] args) throws Exception {
if (args.length > 0 && args[0].equals("hold")) { // used by run.sh: hold nested scopes open so jcmd can dump them
try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow(), cf -> cf.withName("orders"))) {
scope.fork(() -> { Thread.sleep(6000); return "A"; });
scope.fork(() -> { // subtask B opens a child scope of its own
try (var child = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow(), cf -> cf.withName("inventory"))) {
child.fork(() -> { Thread.sleep(6000); return "B1"; });
child.join();
}
return "B";
});
scope.join();
}
return;
}
Path file = Files.createTempFile("dump", ".json"); Files.delete(file);
try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow(), cf -> cf.withName("orders"))) {
for (int i = 0; i < 2; i++) {
int n = i;
scope.fork(() -> { Thread.sleep(1500); return "task-" + n; });
}
Thread.sleep(300); // let the subtasks start and block
ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
.dumpThreads(file.toString(), HotSpotDiagnosticMXBean.ThreadDumpFormat.JSON);
scope.join();
}
String json = Files.readString(file);
int at = json.indexOf("\"container\": \"orders");
int start = json.lastIndexOf('{', at);
int end = json.indexOf("\"threadCount\"", at);
String block = json.substring(start, end);
// identity hashes, thread ids and timestamps change between runs: mask them so the transcript is stable
block = block.replaceAll("@[0-9a-f]+", "@<hash>").replaceAll("\"tid\": \\d+", "\"tid\": <n>").replaceAll("\"owner\": \\d+", "\"owner\": <n>");
// keep the lines that show the hierarchy: the scope, its parent and owner, and each forked thread with its own frame
for (String line : block.split("\n")) {
String l = line.trim();
if (l.startsWith("\"container\"") || l.startsWith("\"parent\"") || l.startsWith("\"owner\"")
|| l.startsWith("\"tid\"") || l.startsWith("\"virtual\"") || l.startsWith("\"state\"")
|| l.contains("ThreadDump.lambda")) {
System.out.println(l);
}
}
Files.deleteIfExists(file);
}
}
@@ -0,0 +1,61 @@
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
/** Deadlines for a whole group of subtasks, and what the Joiner's timeout() method does with them. */
public class Timeouts {
static <T> List<T> runConcurrently(Collection<Callable<T>> tasks, ThreadFactory factory, Duration timeout)
throws ExecutionException, InterruptedException {
try (var scope = StructuredTaskScope.open(Joiner.<T>allSuccessfulOrThrow(),
cf -> cf.withName("orders").withThreadFactory(factory).withTimeout(timeout))) {
tasks.forEach(scope::fork);
return scope.join();
}
}
/** Collects successes; on timeout it hands back what it has instead of failing. */
static class BestEffort<T> implements Joiner<T, List<T>, RuntimeException> {
private final Queue<T> results = new ConcurrentLinkedQueue<>();
public boolean onComplete(Subtask<T> s) {
if (s.state() == Subtask.State.SUCCESS) results.add(s.get());
return false;
}
public List<T> result() { return List.copyOf(results); }
public List<T> timeout() { return List.copyOf(results); } // no exception: a partial answer
}
public static void main(String[] args) throws Exception {
ThreadFactory factory = Thread.ofVirtual().name("order-", 0).factory();
Callable<String> fast = () -> { Thread.sleep(20); return "fast"; };
Callable<String> slow = () -> { Thread.sleep(5000); return "slow"; };
System.out.println("-- inside the deadline");
System.out.println(" " + runConcurrently(List.of(fast, fast), factory, Duration.ofSeconds(1)));
System.out.println("-- deadline 200 ms, one subtask needs 5 s");
long t0 = System.nanoTime();
try {
runConcurrently(List.of(fast, slow), factory, Duration.ofMillis(200));
} catch (ExecutionException e) {
long ms = (System.nanoTime() - t0) / 1_000_000;
System.out.println(" " + e.getClass().getSimpleName() + ", cause = " + e.getCause().getClass().getName());
System.out.println(" returned within " + (ms < 1000 ? "well under a second" : ms + " ms") + " (not 5 s): the slow subtask was interrupted");
}
System.out.println("-- the same deadline with a joiner whose timeout() returns a partial result");
try (var scope = StructuredTaskScope.open(new BestEffort<String>(), cf -> cf.withTimeout(Duration.ofMillis(200)))) {
scope.fork(fast); scope.fork(slow);
System.out.println(" join() = " + scope.join());
}
System.out.println("-- a thread factory decides the subtask thread's name");
try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(), cf -> cf.withThreadFactory(Thread.ofVirtual().name("order-", 0).factory()))) {
scope.fork(() -> Thread.currentThread().getName());
scope.fork(() -> Thread.currentThread().getName());
System.out.println(" " + scope.join());
}
}
}
@@ -0,0 +1,40 @@
import java.util.concurrent.*;
/** The ExecutorService version of handle(): findUser() fails, and fetchOrder() keeps running. */
public class Unstructured {
static String findUser() throws InterruptedException {
Thread.sleep(50);
throw new IllegalStateException("user service down");
}
static int fetchOrder() throws InterruptedException {
try {
Thread.sleep(300);
System.out.println(" fetchOrder() finished, although nobody wants the answer any more");
return 42;
} catch (InterruptedException e) {
System.out.println(" fetchOrder() was interrupted");
throw e;
}
}
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
try {
Future<String> user = executor.submit(Unstructured::findUser);
Future<Integer> order = executor.submit(Unstructured::fetchOrder);
try {
String u = user.get(); // throws after ~50 ms ...
int o = order.get();
System.out.println(u + " " + o);
} catch (ExecutionException e) {
System.out.println("handle() failed: " + e.getCause());
}
System.out.println("handle() has returned. Waiting 400 ms to see what the other thread does:");
Thread.sleep(400);
} finally {
executor.close();
}
}
}