diff a/.jcheck/conf b/.jcheck/conf
--- a/.jcheck/conf
+++ b/.jcheck/conf
@@ -1,41 +1,16 @@
 [general]
-project=jdk
+project=loom
+repository=loom
 jbs=JDK
 version=25
 
 [checks]
-error=author,committer,reviewers,merge,issues,executable,symlink,message,hg-tag,whitespace,problemlists,copyright
-warning=issuestitle,binary
-
-[repository]
-tags=(?:jdk-(?:[1-9]([0-9]*)(?:\.(?:0|[1-9][0-9]*)){0,4})(?:\+(?:(?:[0-9]+))|(?:-ga)))|(?:jdk[4-9](?:u\d{1,3})?-(?:(?:b\d{2,3})|(?:ga)))|(?:hs\d\d(?:\.\d{1,2})?-b\d\d)
-branches=.*
+error=author,committer,whitespace
 
 [census]
 version=0
 domain=openjdk.org
 
 [checks "whitespace"]
 files=.*\.cpp|.*\.hpp|.*\.c|.*\.h|.*\.java|.*\.cc|.*\.hh|.*\.m|.*\.mm|.*\.S|.*\.md|.*\.properties|.*\.gmk|.*\.m4|.*\.ac|Makefile
 ignore-tabs=.*\.gmk|Makefile
-
-[checks "merge"]
-message=Merge
-
-[checks "reviewers"]
-reviewers=1
-ignore=duke
-
-[checks "committer"]
-role=committer
-
-[checks "issues"]
-pattern=^([124-8][0-9]{6}): (\S.*)$
-
-[checks "problemlists"]
-dirs=test/jdk|test/langtools|test/lib-test|test/hotspot/jtreg|test/jaxp
-
-[checks "copyright"]
-files=^(?!LICENSE|license\.txt|.*\.bin|.*\.gif|.*\.jpg|.*\.png|.*\.icon|.*\.tiff|.*\.dat|.*\.patch|.*\.wav|.*\.class|.*-header|.*\.jar|).*
-oracle_locator=.*Copyright \(c\)(.*)Oracle and/or its affiliates\. All rights reserved\.
-oracle_validator=.*Copyright \(c\) (\d{4})(?:, (\d{4}))?, Oracle and/or its affiliates\. All rights reserved\.
diff a/src/hotspot/share/runtime/globals.hpp b/src/hotspot/share/runtime/globals.hpp
--- a/src/hotspot/share/runtime/globals.hpp
+++ b/src/hotspot/share/runtime/globals.hpp
@@ -1900,11 +1900,11 @@
           "Deopt stack after thaw")                                         \
                                                                             \
   develop(bool, LoomVerifyAfterThaw, false,                                 \
           "Verify stack after thaw")                                        \
                                                                             \
-  develop(bool, VerifyContinuations, false,                                 \
+  develop(bool, VerifyContinuations, true,                                  \
           "Verify continuation consistency")                                \
                                                                             \
   develop(bool, UseContinuationFastPath, true,                              \
           "Use fast-path frame walking in continuations")                   \
                                                                             \
diff a/src/hotspot/share/services/diagnosticCommand.hpp b/src/hotspot/share/services/diagnosticCommand.hpp
--- a/src/hotspot/share/services/diagnosticCommand.hpp
+++ b/src/hotspot/share/services/diagnosticCommand.hpp
@@ -354,11 +354,11 @@
 public:
   static int num_arguments() { return 2; }
   ThreadDumpDCmd(outputStream* output, bool heap);
   static const char* name() { return "Thread.print"; }
   static const char* description() {
-    return "Print all threads with stacktraces.";
+    return "Print all platform threads with stacktraces.";
   }
   static const char* impact() {
     return "Medium: Depends on the number of threads.";
   }
   virtual void execute(DCmdSource source, TRAPS);
diff a/src/java.base/share/classes/java/lang/ScopedValue.java b/src/java.base/share/classes/java/lang/ScopedValue.java
--- a/src/java.base/share/classes/java/lang/ScopedValue.java
+++ b/src/java.base/share/classes/java/lang/ScopedValue.java
@@ -155,11 +155,12 @@
  *
  * {@snippet lang=java :
  *     private static final ScopedValue<String> NAME = ScopedValue.newInstance();
 
  *     ScopedValue.where(NAME, "duke").run(() -> {
- *         try (var scope = new StructuredTaskScope<String>()) {
+ *         // @link substring="open" target="StructuredTaskScope#open()" :
+ *         try (var scope = StructuredTaskScope.open()) {
  *
  *              // @link substring="fork" target="StructuredTaskScope#fork(java.util.concurrent.Callable)" :
  *              scope.fork(() -> childTask1());
  *              scope.fork(() -> childTask2());
  *              scope.fork(() -> childTask3());
diff a/src/java.base/share/classes/java/lang/Thread.java b/src/java.base/share/classes/java/lang/Thread.java
--- a/src/java.base/share/classes/java/lang/Thread.java
+++ b/src/java.base/share/classes/java/lang/Thread.java
@@ -188,12 +188,12 @@
  *   <tbody>
  *   <tr>
  *     <th scope="row">
  *       {@systemProperty jdk.virtualThreadScheduler.parallelism}
  *     </th>
- *     <td> The number of platform threads available for scheduling virtual
- *       threads. It defaults to the number of available processors. </td>
+ *     <td> The scheduler's target parallelism. It defaults to the number of
+ *       available processors. </td>
  *   </tr>
  *   <tr>
  *     <th scope="row">
  *       {@systemProperty jdk.virtualThreadScheduler.maxPoolSize}
  *     </th>
diff a/src/java.base/share/classes/java/lang/VirtualThread.java b/src/java.base/share/classes/java/lang/VirtualThread.java
--- a/src/java.base/share/classes/java/lang/VirtualThread.java
+++ b/src/java.base/share/classes/java/lang/VirtualThread.java
@@ -22,10 +22,11 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 package java.lang;
 
+import java.lang.reflect.Constructor;
 import java.util.Arrays;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -63,11 +64,11 @@
  * A thread that is scheduled by the Java virtual machine rather than the operating system.
  */
 final class VirtualThread extends BaseVirtualThread {
     private static final Unsafe U = Unsafe.getUnsafe();
     private static final ContinuationScope VTHREAD_SCOPE = new ContinuationScope("VirtualThreads");
-    private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler();
+    private static final Executor DEFAULT_SCHEDULER = createDefaultScheduler();
     private static final ScheduledExecutorService[] DELAYED_TASK_SCHEDULERS = createDelayedTaskSchedulers();
 
     private static final long STATE = U.objectFieldOffset(VirtualThread.class, "state");
     private static final long PARK_PERMIT = U.objectFieldOffset(VirtualThread.class, "parkPermit");
     private static final long CARRIER_THREAD = U.objectFieldOffset(VirtualThread.class, "carrierThread");
@@ -1408,14 +1409,42 @@
 
         // ensure VTHREAD_GROUP is created, may be accessed by JVMTI
         var group = Thread.virtualThreadGroup();
     }
 
+    /**
+     * Creates the default scheduler.
+     * If the system property {@code jdk.virtualThreadScheduler.implClass} is set then
+     * its value is the name of a class that implements java.util.concurrent.Executor.
+     * The class is public in an exported package, has a public no-arg constructor,
+     * and is visible to the system class loader.
+     * If the system property is not set then the default scheduler will be a
+     * ForkJoinPool instance.
+     */
+    private static Executor createDefaultScheduler() {
+        String propValue = System.getProperty("jdk.virtualThreadScheduler.implClass");
+        if (propValue != null) {
+            try {
+                Class<?> clazz = Class.forName(propValue, true,
+                        ClassLoader.getSystemClassLoader());
+                Constructor<?> ctor = clazz.getConstructor();
+                var scheduler = (Executor) ctor.newInstance();
+                System.err.println("""
+                    WARNING: Using custom scheduler, this is an experimental feature.""");
+                return scheduler;
+            } catch (Exception ex) {
+                throw new Error(ex);
+            }
+        } else {
+            return createDefaultForkJoinPoolScheduler();
+        }
+    }
+
     /**
      * Creates the default ForkJoinPool scheduler.
      */
-    private static ForkJoinPool createDefaultScheduler() {
+    private static ForkJoinPool createDefaultForkJoinPoolScheduler() {
         ForkJoinWorkerThreadFactory factory = pool -> new CarrierThread(pool);
         int parallelism, maxPoolSize, minRunnable;
         String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");
         String maxPoolSizeValue = System.getProperty("jdk.virtualThreadScheduler.maxPoolSize");
         String minRunnableValue = System.getProperty("jdk.virtualThreadScheduler.minRunnable");
diff a/src/java.base/share/classes/java/util/concurrent/Joiners.java b/src/java.base/share/classes/java/util/concurrent/Joiners.java
--- /dev/null
+++ b/src/java.base/share/classes/java/util/concurrent/Joiners.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package java.util.concurrent;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.concurrent.StructuredTaskScope.Joiner;
+import java.util.concurrent.StructuredTaskScope.Subtask;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import jdk.internal.invoke.MhUtil;
+
+/**
+ * Built-in StructuredTaskScope.Joiner implementations.
+ */
+class Joiners {
+    private Joiners() { }
+
+    /**
+     * A joiner that returns a stream of all subtasks when all subtasks complete
+     * successfully. Cancels the scope if any subtask fails.
+     */
+    static final class AllSuccessful<T> implements Joiner<T, Stream<Subtask<T>>> {
+        private static final VarHandle FIRST_EXCEPTION =
+                MhUtil.findVarHandle(MethodHandles.lookup(), "firstException", Throwable.class);
+
+        // list of forked subtasks, only accessed by owner thread
+        private final List<Subtask<T>> subtasks = new ArrayList<>();
+
+        private volatile Throwable firstException;
+
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            if (subtask.state() != Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            @SuppressWarnings("unchecked")
+            var s = (Subtask<T>) subtask;
+            subtasks.add(s);
+            return false;
+        }
+
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            Subtask.State state = subtask.state();
+            if (state == Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            return (state == Subtask.State.FAILED)
+                    && (firstException == null)
+                    && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
+        }
+
+        @Override
+        public Stream<Subtask<T>> result() throws Throwable {
+            Throwable ex = firstException;
+            if (ex != null) {
+                throw ex;
+            } else {
+                return subtasks.stream();
+            }
+        }
+    }
+
+    /**
+     * A joiner that returns the result of the first subtask to complete successfully.
+     * Cancels the scope if any subtasks succeeds.
+     */
+    static final class AnySuccessful<T> implements Joiner<T, T> {
+        private static final VarHandle SUBTASK =
+                MhUtil.findVarHandle(MethodHandles.lookup(), "subtask", Subtask.class);
+
+        // UNAVAILABLE < FAILED < SUCCESS
+        private static final Comparator<Subtask.State> SUBTASK_STATE_COMPARATOR =
+                Comparator.comparingInt(AnySuccessful::stateToInt);
+
+        private volatile Subtask<T> subtask;
+
+        /**
+         * Maps a Subtask.State to an int that can be compared.
+         */
+        private static int stateToInt(Subtask.State s) {
+            return switch (s) {
+                case UNAVAILABLE -> 0;
+                case FAILED      -> 1;
+                case SUCCESS     -> 2;
+            };
+        }
+
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            Subtask.State state = subtask.state();
+            if (state == Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            Subtask<T> s;
+            while (((s = this.subtask) == null)
+                    || SUBTASK_STATE_COMPARATOR.compare(s.state(), state) < 0) {
+                if (SUBTASK.compareAndSet(this, s, subtask)) {
+                    return (state == Subtask.State.SUCCESS);
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public T result() throws Throwable {
+            Subtask<T> subtask = this.subtask;
+            if (subtask == null) {
+                throw new NoSuchElementException("No subtasks completed");
+            }
+            return switch (subtask.state()) {
+                case SUCCESS -> subtask.get();
+                case FAILED -> throw subtask.exception();
+                default -> throw new InternalError();
+            };
+        }
+    }
+
+    /**
+     * A joiner that that waits for all successful subtasks. Cancels the scope if any
+     * subtask fails.
+     */
+    static final class AwaitSuccessful<T> implements Joiner<T, Void> {
+        private static final VarHandle FIRST_EXCEPTION =
+                MhUtil.findVarHandle(MethodHandles.lookup(), "firstException", Throwable.class);
+        private volatile Throwable firstException;
+
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            Subtask.State state = subtask.state();
+            if (state == Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            return (state == Subtask.State.FAILED)
+                    && (firstException == null)
+                    && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
+        }
+
+        @Override
+        public Void result() throws Throwable {
+            Throwable ex = firstException;
+            if (ex != null) {
+                throw ex;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * A joiner that returns a stream of all subtasks.
+     */
+    static class AllSubtasks<T> implements Joiner<T, Stream<Subtask<T>>> {
+        private final Predicate<Subtask<? extends T>> isDone;
+
+        // list of forked subtasks, only accessed by owner thread
+        private final List<Subtask<T>> subtasks = new ArrayList<>();
+
+        AllSubtasks(Predicate<Subtask<? extends T>> isDone) {
+            this.isDone = Objects.requireNonNull(isDone);
+        }
+
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            if (subtask.state() != Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            @SuppressWarnings("unchecked")
+            var s = (Subtask<T>) subtask;
+            subtasks.add(s);
+            return false;
+        }
+
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            if (subtask.state() == Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            return isDone.test(subtask);
+        }
+
+        @Override
+        public Stream<Subtask<T>> result() {
+            return subtasks.stream();
+        }
+    }
+}
diff a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java
--- a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java
+++ b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java
@@ -22,1284 +22,1050 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 package java.util.concurrent;
 
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.VarHandle;
 import java.time.Duration;
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 import jdk.internal.javac.PreviewFeature;
-import jdk.internal.misc.ThreadFlock;
-import jdk.internal.invoke.MhUtil;
 
 /**
- * A basic API for <em>structured concurrency</em>. {@code StructuredTaskScope} supports
- * cases where a task splits into several concurrent subtasks, and where the subtasks must
- * complete before the main task continues. A {@code StructuredTaskScope} can be used to
- * ensure that the lifetime of a concurrent operation is confined by a <em>syntax block</em>,
- * just like that of a sequential operation in structured programming.
+ * An API for <em>structured concurrency</em>. {@code StructuredTaskScope} supports cases
+ * where execution of a <em>task</em> (a unit of work) splits into several concurrent
+ * subtasks, and where the subtasks must complete before the task continues. A {@code
+ * StructuredTaskScope} can be used to ensure that the lifetime of a concurrent operation
+ * is confined by a <em>syntax block</em>, similar to that of a sequential operation in
+ * structured programming.
  *
- * <h2>Basic operation</h2>
+ * <p> {@code StructuredTaskScope} defines the static method {@link #open() open} to open
+ * a new {@code StructuredTaskScope} and the {@link #close() close} method to close it.
+ * The API is designed to be used with the {@code try}-with-resources statement where
+ * the {@code StructuredTaskScope} is opened as a resource and then closed automatically.
+ * The code inside the block uses the {@link #fork(Callable) fork} method to fork subtasks.
+ * After forking, it uses the {@link #join() join} method to wait for all subtasks to
+ * finish (or some other outcome) as a single operation. Forking a subtask starts a new
+ * {@link Thread} to run the subtask. The thread executing the task does not continue
+ * beyond the {@code close} method until all threads started to execute subtasks have finished.
+ * To ensure correct usage, the {@code fork}, {@code join} and {@code close} methods may
+ * only be invoked by the <em>owner thread</em> (the thread that opened the {@code
+ * StructuredTaskScope}), the {@code fork} method may not be called after {@code join},
+ * the {@code join} method may only be invoked once, and the {@code close} method throws
+ * an exception after closing if the owner did not invoke the {@code join} method after
+ * forking subtasks.
  *
- * A {@code StructuredTaskScope} is created with one of its public constructors. It defines
- * the {@link #fork(Callable) fork} method to start a thread to execute a subtask, the {@link
- * #join() join} method to wait for all subtasks to finish, and the {@link #close() close}
- * method to close the task scope. The API is intended to be used with the {@code
- * try-with-resources} statement. The intention is that code in the try <em>block</em>
- * uses the {@code fork} method to fork threads to execute the subtasks, wait for the
- * subtasks to finish with the {@code join} method, and then <em>process the results</em>.
- * A call to the {@code fork} method returns a {@link Subtask Subtask} to representing
- * the <em>forked subtask</em>. Once {@code join} is called, the {@code Subtask} can be
- * used to get the result completed successfully, or the exception if the subtask failed.
+ * <p> As a first example, consider a task that splits into two subtasks to concurrently
+ * fetch resources from two URL locations "left" and "right". Both subtasks may complete
+ * successfully, one subtask may succeed and the other may fail, or both subtasks may
+ * fail. The task in this example is interested in the successful result from both
+ * subtasks. It waits in the {@link #join() join} method for both subtasks to complete
+ * successfully or for either subtask to fail.
  * {@snippet lang=java :
- *     Callable<String> task1 = ...
- *     Callable<Integer> task2 = ...
+ *    // @link substring="open" target="#open()" :
+ *    try (var scope = StructuredTaskScope.open()) {
  *
- *     try (var scope = new StructuredTaskScope<Object>()) {
+ *        // @link substring="fork" target="#fork(Callable)" :
+ *        Subtask<String> subtask1 = scope.fork(() -> query(left));
+ *        Subtask<Integer> subtask2 = scope.fork(() -> query(right));
  *
- *         Subtask<String> subtask1 = scope.fork(task1);   // @highlight substring="fork"
- *         Subtask<Integer> subtask2 = scope.fork(task2);  // @highlight substring="fork"
+ *        // throws if either subtask fails
+ *        scope.join();  // @link substring="join" target="#join()"
  *
- *         scope.join();                                   // @highlight substring="join"
+ *        // both subtasks completed successfully
+ *        // @link substring="get" target="Subtask#get()" :
+ *        return new MyResult(subtask1.get(), subtask2.get());
  *
- *         ... process results/exceptions ...
- *
- *     } // close                                          // @highlight substring="close"
+ *    // @link substring="close" target="#close()" :
+ *    } // close
  * }
- * <p> The following example forks a collection of homogeneous subtasks, waits for all of
- * them to complete with the {@code join} method, and uses the {@link Subtask.State
- * Subtask.State} to partition the subtasks into a set of the subtasks that completed
- * successfully and another for the subtasks that failed.
- * {@snippet lang=java :
- *     List<Callable<String>> callables = ...
  *
- *     try (var scope = new StructuredTaskScope<String>()) {
+ * <p> If both subtasks complete successfully then the {@code join} method completes
+ * normally and the task uses the {@link Subtask#get() Subtask.get()} method to get
+ * the result of each subtask. If one of the subtasks fails then the other subtask is
+ * cancelled (this will {@linkplain Thread#interrupt() interrupt} the thread executing the
+ * other subtask) and the {@code join} method throws {@link FailedException} with the
+ * exception from the failed subtask as the {@linkplain Throwable#getCause() cause}.
  *
- *         List<Subtask<String>> subtasks = callables.stream().map(scope::fork).toList();
+ * <p> To allow for cancellation, subtasks must be coded so that they finish as soon as
+ * possible when interrupted. Subtasks that do not respond to interrupt, e.g. block on
+ * methods that are not interruptible, may delay the closing of a scope indefinitely. The
+ * {@link #close() close} method always waits for threads executing subtasks to finish,
+ * even if the scope is cancelled, so execution cannot continue beyond the {@code close}
+ * method until the interrupted threads finish.
  *
- *         scope.join();
+ * <p> In the example, the subtasks produce results of different types ({@code String} and
+ * {@code Integer}). In other cases the subtasks may all produce results of the same type.
+ * If the example had used {@code StructuredTaskScope.<String>open()} then it could
+ * only be used to fork subtasks that return a {@code String} result.
  *
- *         Map<Boolean, Set<Subtask<String>>> map = subtasks.stream()
- *                 .collect(Collectors.partitioningBy(h -> h.state() == Subtask.State.SUCCESS,
- *                                                    Collectors.toSet()));
+ * <h2>Joiners</h2>
  *
- *     } // close
- * }
+ * <p> In the example above, the task fails if any subtask fails. If all subtasks
+ * succeed then the {@code join} method completes normally. Other policy and outcome is
+ * supported by creating a {@code StructuredTaskScope} with a {@link Joiner} that
+ * implements the desired policy. A {@code Joiner} handles subtask completion and produces
+ * the outcome for the {@link #join() join} method. In the example above, {@code join}
+ * returns {@code null}. Depending on the {@code Joiner}, {@code join} may return a
+ * result, a stream of elements, or some other object. The {@code Joiner} interface defines
+ * factory methods to create {@code Joiner}s for some common cases.
+ *
+ * <p> A {@code Joiner} may <a id="Cancallation">cancel</a> the scope (sometimes called
+ * "short-circuiting") when some condition is reached that does not require the result of
+ * subtasks that are still executing. Cancelling the scope prevents new threads from being
+ * started to execute further subtasks, {@linkplain Thread#interrupt() interrupts} the
+ * threads executing subtasks that have not completed, and causes the {@code join} method
+ * to wakeup with the outcome (result or exception). In the above example, the outcome is
+ * that {@code join} completes with a result of {@code null} when all subtasks succeed.
+ * The scope is cancelled if any of the subtasks fail and {@code join} throws {@code
+ * FailedException} with the exception from the failed subtask as the cause. Other {@code
+ * Joiner} implementations may cancel the scope for other reasons.
+ *
+ * <p> Now consider another example that splits into two subtasks. In this example,
+ * each subtask produces a {@code String} result and the task is only interested in
+ * the result from the first subtask to complete successfully. The example uses {@link
+ * Joiner#anySuccessfulResultOrThrow() Joiner.anySuccessfulResultOrThrow()} to
+ * create a {@code Joiner} that makes available the result of the first subtask to
+ * complete successfully. The type parameter in the example is "{@code String}" so that
+ * only subtasks that return a {@code String} can be forked.
+ * {@snippet lang=java :
+ *    // @link substring="open" target="#open(Joiner)" :
+ *    try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow())) {
  *
- * <p> To ensure correct usage, the {@code join} and {@code close} methods may only be
- * invoked by the <em>owner</em> (the thread that opened/created the task scope), and the
- * {@code close} method throws an exception after closing if the owner did not invoke the
- * {@code join} method after forking.
+ *        scope.fork(callable1);
+ *        scope.fork(callable2);
  *
- * <p> {@code StructuredTaskScope} defines the {@link #shutdown() shutdown} method to shut
- * down a task scope without closing it. The {@code shutdown()} method <em>cancels</em> all
- * unfinished subtasks by {@linkplain Thread#interrupt() interrupting} the threads. It
- * prevents new threads from starting in the task scope. If the owner is waiting in the
- * {@code join} method then it will wakeup.
+ *        // throws if both subtasks fail
+ *        String firstResult = scope.join();
  *
- * <p> Shutdown is used for <em>short-circuiting</em> and allow subclasses to implement
- * <em>policy</em> that does not require all subtasks to finish.
+ *    }
+ * }
+ *
+ * <p> In the example, the task forks the two subtasks, then waits in the {@code
+ * join} method for either subtask to complete successfully or for both subtasks to fail.
+ * If one of the subtasks completes successfully then the {@code Joiner} causes the other
+ * subtask to be cancelled (this will interrupt the thread executing the subtask), and
+ * the {@code join} method returns the result from the successful subtask. Cancelling the
+ * other subtask avoids the task waiting for a result that it doesn't care about. If
+ * both subtasks fail then the {@code join} method throws {@code FailedException} with the
+ * exception from one of the subtasks as the {@linkplain Throwable#getCause() cause}.
+ *
+ * <p> Whether code uses the {@code Subtask} returned from {@code fork} will depend on
+ * the {@code Joiner} and usage. Some {@code Joiner} implementations are suited to subtasks
+ * that return results of the same type and where the {@code join} method returns a result
+ * for the task to use. Code that forks subtasks that return results of different
+ * types, and uses a {@code Joiner} such as {@code Joiner.awaitAllSuccessfulOrThrow()} that
+ * does not return a result, will use {@link Subtask#get() Subtask.get()} after joining.
  *
- * <h2>Subclasses with policies for common cases</h2>
+ * <h2>Exception handling</h2>
  *
- * Two subclasses of {@code StructuredTaskScope} are defined to implement policy for
- * common cases:
- * <ol>
- *   <li> {@link ShutdownOnSuccess ShutdownOnSuccess} captures the result of the first
- *   subtask to complete successfully. Once captured, it shuts down the task scope to
- *   interrupt unfinished threads and wakeup the owner. This class is intended for cases
- *   where the result of any subtask will do ("invoke any") and where there is no need to
- *   wait for results of other unfinished subtasks. It defines methods to get the first
- *   result or throw an exception if all subtasks fail.
- *   <li> {@link ShutdownOnFailure ShutdownOnFailure} captures the exception of the first
- *   subtask to fail. Once captured, it shuts down the task scope to interrupt unfinished
- *   threads and wakeup the owner. This class is intended for cases where the results of all
- *   subtasks are required ("invoke all"); if any subtask fails then the results of other
- *   unfinished subtasks are no longer needed. If defines methods to throw an exception if
- *   any of the subtasks fail.
- * </ol>
+ * <p> A {@code StructuredTaskScope} is opened with a {@link Joiner Joiner} that
+ * handles subtask completion and produces the outcome for the {@link #join() join} method.
+ * In some cases, the outcome will be a result, in other cases it will be an exception.
+ * If the outcome is an exception then the {@code join} method throws {@link
+ * FailedException} with the exception as the {@linkplain Throwable#getCause()
+ * cause}. For many {@code Joiner} implementations, the exception will be an exception
+ * thrown by a subtask that failed. In the case of {@link Joiner#allSuccessfulOrThrow()
+ * allSuccessfulOrThrow} and {@link Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow}
+ * for example, the exception is from the first subtask to fail.
  *
- * <p> The following are two examples that use the two classes. In both cases, a pair of
- * subtasks are forked to fetch resources from two URL locations "left" and "right". The
- * first example creates a ShutdownOnSuccess object to capture the result of the first
- * subtask to complete successfully, cancelling the other by way of shutting down the task
- * scope. The main task waits in {@code join} until either subtask completes with a result
- * or both subtasks fail. It invokes {@link ShutdownOnSuccess#result(Function)
- * result(Function)} method to get the captured result. If both subtasks fail then this
- * method throws a {@code WebApplicationException} with the exception from one of the
- * subtasks as the cause.
+ * <p> Many of the details for how exceptions are handled will depend on usage. In some
+ * cases it may be useful to add a {@code catch} block to the {@code try}-with-resources
+ * statement to catch {@code FailedException}. The exception handling may use {@code
+ * instanceof} with pattern matching to handle specific causes.
  * {@snippet lang=java :
- *     try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
+ *    try (var scope = StructuredTaskScope.open()) {
  *
- *         scope.fork(() -> fetch(left));
- *         scope.fork(() -> fetch(right));
+ *        ..
  *
- *         scope.join();
+ *    } catch (StructuredTaskScope.FailedException e) {
  *
- *         // @link regex="result(?=\()" target="ShutdownOnSuccess#result" :
- *         String result = scope.result(e -> new WebApplicationException(e));
+ *        Throwable cause = e.getCause();
+ *        switch (cause) {
+ *            case IOException ioe -> ..
+ *            default -> ..
+ *        }
  *
- *         ...
- *     }
+ *    }
  * }
- * The second example creates a ShutdownOnFailure object to capture the exception of the
- * first subtask to fail, cancelling the other by way of shutting down the task scope. The
- * main task waits in {@link #joinUntil(Instant)} until both subtasks complete with a
- * result, either fails, or a deadline is reached. It invokes {@link
- * ShutdownOnFailure#throwIfFailed(Function) throwIfFailed(Function)} to throw an exception
- * if either subtask fails. This method is a no-op if both subtasks complete successfully.
- * The example uses {@link Supplier#get()} to get the result of each subtask. Using
- * {@code Supplier} instead of {@code Subtask} is preferred for common cases where the
- * object returned by fork is only used to get the result of a subtask that completed
- * successfully.
- * {@snippet lang=java :
- *    Instant deadline = ...
+ * In other cases it may not be useful to catch {@code FailedException} but instead leave
+ * it to propagate to the configured {@linkplain Thread.UncaughtExceptionHandler uncaught
+ * exception handler} for logging purposes.
  *
- *    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
+ * <p> For cases where a specific exception triggers the use of a default result then it
+ * may be more appropriate to handle this in the subtask itself rather than the subtask
+ * failing and the scope owner handling the exception.
  *
- *         Supplier<String> supplier1 = scope.fork(() -> query(left));
- *         Supplier<String> supplier2 = scope.fork(() -> query(right));
+ * <h2>Configuration</h2>
  *
- *         scope.joinUntil(deadline);
+ * A {@code StructuredTaskScope} is opened with {@linkplain Config configuration} that
+ * consists of a {@link ThreadFactory} to create threads, an optional name for monitoring
+ * and management purposes, and an optional timeout.
  *
- *         // @link substring="throwIfFailed" target="ShutdownOnFailure#throwIfFailed" :
- *         scope.throwIfFailed(e -> new WebApplicationException(e));
+ * <p> The {@link #open()} and {@link #open(Joiner)} methods create a {@code StructuredTaskScope}
+ * with the <a id="DefaultConfiguration"> <em>default configuration</em></a>. The default
+ * configuration has a {@code ThreadFactory} that creates unnamed
+ * <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual threads</a>,
+ * is unnamed for monitoring and management purposes, and has no timeout.
  *
- *         // both subtasks completed successfully
- *         String result = Stream.of(supplier1, supplier2)
- *                 .map(Supplier::get)
- *                 .collect(Collectors.joining(", ", "{ ", " }"));
+ * <p> The 2-arg {@link #open(Joiner, Function) open} method can be used to create a
+ * {@code StructuredTaskScope} that uses a different {@code ThreadFactory}, has a name for
+ * the purposes of monitoring and management, or has a timeout that cancels the scope if
+ * the timeout expires before or while waiting for subtasks to complete. The {@code open}
+ * method is called with a {@linkplain Function function} that is applied to the default
+ * configuration and returns a {@link Config Config} for the {@code StructuredTaskScope}
+ * under construction.
  *
- *         ...
- *     }
- * }
+ * <p> The following example opens a new {@code StructuredTaskScope} with a {@code
+ * ThreadFactory} that creates virtual threads {@linkplain Thread#setName(String) named}
+ * "duke-0", "duke-1" ...
+ * {@snippet lang = java:
+ *    // @link substring="name" target="Thread.Builder#name(String, long)" :
+ *    ThreadFactory factory = Thread.ofVirtual().name("duke-", 0).factory();
+ *
+ *    // @link substring="withThreadFactory" target="Config#withThreadFactory(ThreadFactory)" :
+ *    try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) {
  *
- * <h2>Extending StructuredTaskScope</h2>
+ *        scope.fork( .. );   // runs in a virtual thread with name "duke-0"
+ *        scope.fork( .. );   // runs in a virtual thread with name "duke-1"
  *
- * {@code StructuredTaskScope} can be extended, and the {@link #handleComplete(Subtask)
- * handleComplete} method overridden, to implement policies other than those implemented
- * by {@code ShutdownOnSuccess} and {@code ShutdownOnFailure}. A subclass may, for example,
- * collect the results of subtasks that complete successfully and ignore subtasks that
- * fail. It may collect exceptions when subtasks fail. It may invoke the {@link #shutdown()
- * shutdown} method to shut down and cause {@link #join() join} to wakeup when some
- * condition arises.
+ *        scope.join();
  *
- * <p> A subclass will typically define methods to make available results, state, or other
- * outcome to code that executes after the {@code join} method. A subclass that collects
- * results and ignores subtasks that fail may define a method that returns the results.
- * A subclass that implements a policy to shut down when a subtask fails may define a
- * method to get the exception of the first subtask to fail.
+ *     }
+ *}
  *
- * <p> The following is an example of a simple {@code StructuredTaskScope} implementation
- * that collects homogenous subtasks that complete successfully. It defines the method
- * "{@code completedSuccessfully()}" that the main task can invoke after it joins.
+ * <p> A second example sets a timeout, represented by a {@link Duration}. The timeout
+ * starts when the new scope is opened. If the timeout expires before the {@code join}
+ * method has completed then the scope is <a href="#Cancallation">cancelled</a>. This
+ * interrupts the threads executing the two subtasks and causes the {@link #join() join}
+ * method to throw {@link TimeoutException}.
  * {@snippet lang=java :
- *     class CollectingScope<T> extends StructuredTaskScope<T> {
- *         private final Queue<Subtask<? extends T>> subtasks = new LinkedTransferQueue<>();
+ *    Duration timeout = Duration.ofSeconds(10);
  *
- *         @Override
- *         protected void handleComplete(Subtask<? extends T> subtask) {
- *             if (subtask.state() == Subtask.State.SUCCESS) {
- *                 subtasks.add(subtask);
- *             }
- *         }
+ *    // @link substring="allSuccessfulOrThrow" target="Joiner#allSuccessfulOrThrow()" :
+ *    try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
+ *    // @link substring="withTimeout" target="Config#withTimeout(Duration)" :
+ *                                              cf -> cf.withTimeout(timeout))) {
  *
- *         @Override
- *         public CollectingScope<T> join() throws InterruptedException {
- *             super.join();
- *             return this;
- *         }
+ *        scope.fork(callable1);
+ *        scope.fork(callable2);
  *
- *         public Stream<Subtask<? extends T>> completedSuccessfully() {
- *             // @link substring="ensureOwnerAndJoined" target="ensureOwnerAndJoined" :
- *             super.ensureOwnerAndJoined();
- *             return subtasks.stream();
- *         }
- *     }
+ *        List<String> result = scope.join()
+ *                                   .map(Subtask::get)
+ *                                   .toList();
+ *
+ *   }
  * }
- * <p> The implementations of the {@code completedSuccessfully()} method in the example
- * invokes {@link #ensureOwnerAndJoined()} to ensure that the method can only be invoked
- * by the owner thread and only after it has joined.
  *
- * <h2><a id="TreeStructure">Tree structure</a></h2>
+ * <h2>Inheritance of scoped value bindings</h2>
+ *
+ * {@link ScopedValue} supports the execution of a method with a {@code ScopedValue} bound
+ * to a value for the bounded period of execution of the method by the <em>current thread</em>.
+ * It allows a value to be safely and efficiently shared to methods without using method
+ * parameters.
  *
- * Task scopes form a tree where parent-child relations are established implicitly when
- * opening a new task scope:
- * <ul>
- *   <li> A parent-child relation is established when a thread started in a task scope
- *   opens its own task scope. A thread started in task scope "A" that opens task scope
- *   "B" establishes a parent-child relation where task scope "A" is the parent of task
- *   scope "B".
- *   <li> A parent-child relation is established with nesting. If a thread opens task
- *   scope "B", then opens task scope "C" (before it closes "B"), then the enclosing task
- *   scope "B" is the parent of the nested task scope "C".
- * </ul>
+ * <p> When used in conjunction with a {@code StructuredTaskScope}, a {@code ScopedValue}
+ * can also safely and efficiently share a value to methods executed by subtasks forked
+ * in the scope. When a {@code ScopedValue} object is bound to a value in the thread
+ * executing the task then that binding is inherited by the threads created to
+ * execute the subtasks. The thread executing the task does not continue beyond the
+ * {@link #close() close} method until all threads executing the subtasks have finished.
+ * This ensures that the {@code ScopedValue} is not reverted to being {@linkplain
+ * ScopedValue#isBound() unbound} (or its previous value) while subtasks are executing.
+ * In addition to providing a safe and efficient means to inherit a value into subtasks,
+ * the inheritance allows sequential code using {@code ScopedValue} be refactored to use
+ * structured concurrency.
  *
- * The <i>descendants</i> of a task scope are the child task scopes that it is a parent
- * of, plus the descendants of the child task scopes, recursively.
+ * <p> To ensure correctness, opening a new {@code StructuredTaskScope} captures the
+ * current thread's scoped value bindings. These are the scoped values bindings that are
+ * inherited by the threads created to execute subtasks in the scope. Forking a
+ * subtask checks that the bindings in effect at the time that the subtask is forked
+ * match the bindings when the {@code StructuredTaskScope} was created. This check ensures
+ * that a subtask does not inherit a binding that is reverted in the task before the
+ * subtask has completed.
  *
- * <p> The tree structure supports:
- * <ul>
- *   <li> Inheritance of {@linkplain ScopedValue scoped values} across threads.
- *   <li> Confinement checks. The phrase "threads contained in the task scope" in method
- *   descriptions means threads started in the task scope or descendant scopes.
- * </ul>
+ * <p> A {@code ScopedValue} that is shared across threads requires that the value be an
+ * immutable object or for all access to the value to be appropriately synchronized.
  *
- * <p> The following example demonstrates the inheritance of a scoped value. A scoped
- * value {@code USERNAME} is bound to the value "{@code duke}". A {@code StructuredTaskScope}
- * is created and its {@code fork} method invoked to start a thread to execute {@code
- * childTask}. The thread inherits the scoped value <em>bindings</em> captured when
- * creating the task scope. The code in {@code childTask} uses the value of the scoped
- * value and so reads the value "{@code duke}".
+ * <p> The following example demonstrates the inheritance of scoped value bindings. The
+ * scoped value USERNAME is bound to the value "duke" for the bounded period of a lambda
+ * expression by the thread executing it. The code in the block opens a {@code
+ * StructuredTaskScope} and forks two subtasks, it then waits in the {@code join} method
+ * and aggregates the results from both subtasks. If code executed by the threads
+ * running subtask1 and subtask2 uses {@link ScopedValue#get()}, to get the value of
+ * USERNAME, then value "duke" will be returned.
  * {@snippet lang=java :
+ *     // @link substring="newInstance" target="ScopedValue#newInstance()" :
  *     private static final ScopedValue<String> USERNAME = ScopedValue.newInstance();
  *
- *     // @link substring="run" target="ScopedValue.Carrier#run(Runnable)" :
- *     ScopedValue.where(USERNAME, "duke").run(() -> {
- *         try (var scope = new StructuredTaskScope<String>()) {
+ *     // @link substring="where" target="ScopedValue#where(ScopedValue, Object)" :
+ *     MyResult result = ScopedValue.where(USERNAME, "duke").call(() -> {
  *
- *             scope.fork(() -> childTask());           // @highlight substring="fork"
- *             ...
- *          }
- *     });
+ *         try (var scope = StructuredTaskScope.open()) {
  *
- *     ...
+ *             Subtask<String> subtask1 = scope.fork( .. );    // inherits binding
+ *             Subtask<Integer> subtask2 = scope.fork( .. );   // inherits binding
  *
- *     String childTask() {
- *         // @link substring="get" target="ScopedValue#get()" :
- *         String name = USERNAME.get();   // "duke"
- *         ...
- *     }
+ *             scope.join();
+ *             return new MyResult(subtask1.get(), subtask2.get());
+ *         }
+ *
+ *     });
  * }
  *
- * <p> {@code StructuredTaskScope} does not define APIs that exposes the tree structure
- * at this time.
+ * <p> A scoped value inherited into a subtask may be
+ * <a href="{@docRoot}/java.base/java/lang/ScopedValue.html#rebind">rebound</a> to a new
+ * value in the subtask for the bounded execution of some method executed in the subtask.
+ * When the method completes, the value of the {@code ScopedValue} reverts to its previous
+ * value, the value inherited from the thread executing the task.
  *
- * <p> Unless otherwise specified, passing a {@code null} argument to a constructor
- * or method in this class will cause a {@link NullPointerException} to be thrown.
+ * <p> A subtask may execute code that itself opens a new {@code StructuredTaskScope}.
+ * A task executing in thread T1 opens a {@code StructuredTaskScope} and forks a
+ * subtask that runs in thread T2. The scoped value bindings captured when T1 opens the
+ * scope are inherited into T2. The subtask (in thread T2) executes code that opens a
+ * new {@code StructuredTaskScope} and forks a subtask that runs in thread T3. The scoped
+ * value bindings captured when T2 opens the scope are inherited into T3. These
+ * include (or may be the same) as the bindings that were inherited from T1. In effect,
+ * scoped values are inherited into a tree of subtasks, not just one level of subtask.
  *
  * <h2>Memory consistency effects</h2>
  *
- * <p> Actions in the owner thread of, or a thread contained in, the task scope prior to
+ * <p> Actions in the owner thread of a {@code StructuredTaskScope} prior to
  * {@linkplain #fork forking} of a subtask
  * <a href="{@docRoot}/java.base/java/util/concurrent/package-summary.html#MemoryVisibility">
- * <i>happen-before</i></a> any actions taken by that subtask, which in turn <i>happen-before</i>
- * the subtask result is {@linkplain Subtask#get() retrieved} or <i>happen-before</i> any
- * actions taken in a thread after {@linkplain #join() joining} of the task scope.
+ * <i>happen-before</i></a> any actions taken by that subtask, which in turn
+ * <i>happen-before</i> the subtask result is {@linkplain Subtask#get() retrieved}.
  *
- * @jls 17.4.5 Happens-before Order
+ * <h2>General exceptions</h2>
  *
- * @param <T> the result type of tasks executed in the task scope
+ * <p> Unless otherwise specified, passing a {@code null} argument to a method in this
+ * class will cause a {@link NullPointerException} to be thrown.
+ *
+ * @param <T> the result type of subtasks executed in the scope
+ * @param <R> the result type of the scope
+ *
+ * @jls 17.4.5 Happens-before Order
  * @since 21
  */
 @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-public class StructuredTaskScope<T> implements AutoCloseable {
-    private final ThreadFactory factory;
-    private final ThreadFlock flock;
-    private final ReentrantLock shutdownLock = new ReentrantLock();
-
-    // states: OPEN -> SHUTDOWN -> CLOSED
-    private static final int OPEN     = 0;   // initial state
-    private static final int SHUTDOWN = 1;
-    private static final int CLOSED   = 2;
-
-    // state: set to SHUTDOWN by any thread, set to CLOSED by owner, read by any thread
-    private volatile int state;
-
-    // Counters to support checking that the task scope owner joins before processing
-    // results and attempts join before closing the task scope. These counters are
-    // accessed only by the owner thread.
-    private int forkRound;         // incremented when the first subtask is forked after join
-    private int lastJoinAttempted; // set to the current fork round when join is attempted
-    private int lastJoinCompleted; // set to the current fork round when join completes
+public sealed interface StructuredTaskScope<T, R>
+        extends AutoCloseable
+        permits StructuredTaskScopeImpl {
 
     /**
-     * Represents a subtask forked with {@link #fork(Callable)}.
+     * Represents a subtask forked with {@link #fork(Callable)} or {@link #fork(Runnable)}.
+     *
+     * <p> Code that forks subtasks can use the {@link #get() get()} method after {@linkplain
+     * #join() joining} to obtain the result of a subtask that completed successfully. It
+     * can use the {@link #exception()} method to obtain the exception thrown by a subtask
+     * that failed.
+     *
      * @param <T> the result type
      * @since 21
      */
     @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-    public sealed interface Subtask<T> extends Supplier<T> permits SubtaskImpl {
-        /**
-         * {@return the value returning task provided to the {@code fork} method}
-         *
-         * @apiNote Task objects with unique identity may be used for correlation by
-         * implementations of {@link #handleComplete(Subtask) handleComplete}.
-         */
-        Callable<? extends T> task();
-
+    sealed interface Subtask<T> extends Supplier<T> permits StructuredTaskScopeImpl.SubtaskImpl {
         /**
          * Represents the state of a subtask.
          * @see Subtask#state()
          * @since 21
          */
         @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
         enum State {
             /**
              * The subtask result or exception is not available. This state indicates that
-             * the subtask was forked but has not completed, it completed after the task
-             * scope was {@linkplain #shutdown() shut down}, or it was forked after the
-             * task scope was shut down.
+             * the subtask was forked but has not completed, it completed after the scope
+             * was cancelled, or it was forked after the scoped was cancelled (in which
+             * case a thread was not created to execute the subtask).
              */
             UNAVAILABLE,
             /**
-             * The subtask completed successfully with a result. The {@link Subtask#get()
-             * Subtask.get()} method can be used to obtain the result. This is a terminal
-             * state.
+             * The subtask completed successfully. The {@link Subtask#get() Subtask.get()}
+             * method can be used to get the result. This is a terminal state.
              */
             SUCCESS,
             /**
              * The subtask failed with an exception. The {@link Subtask#exception()
-             * Subtask.exception()} method can be used to obtain the exception. This is a
+             * Subtask.exception()} method can be used to get the exception. This is a
              * terminal state.
              */
             FAILED,
         }
 
         /**
-         * {@return the state of the subtask}
+         * {@return the subtask state}
          */
         State state();
 
         /**
-         * Returns the result of the subtask.
+         * Returns the result of this subtask if it completed successfully. If the subtask
+         * was forked with {@link #fork(Callable) fork(Callable)} then the result from the
+         * {@link Callable#call() call} method is returned. If the subtask was forked with
+         * {@link #fork(Runnable) fork(Runnable)} then {@code null} is returned.
          *
-         * <p> To ensure correct usage, if the scope owner {@linkplain #fork(Callable) forks}
-         * a subtask, then it must join (with {@link #join() join} or {@link #joinUntil(Instant)
-         * joinUntil}) before it can obtain the result of the subtask.
+         * <p> Code executing in the scope owner thread can use this method to get the
+         * result of a successful subtask only after it has {@linkplain #join() joined}.
+         *
+         * <p> Code executing in the {@code Joiner} {@link Joiner#onComplete(Subtask)
+         * onComplete} method should test that the {@linkplain #state() subtask state} is
+         * {@link State#SUCCESS SUCCESS} before using this method to get the result.
          *
          * @return the possibly-null result
          * @throws IllegalStateException if the subtask has not completed, did not complete
-         * successfully, or the current thread is the task scope owner and did not join
-         * after forking
+         * successfully, or the current thread is the scope owner invoking this
+         * method before {@linkplain #join() joining}
          * @see State#SUCCESS
          */
         T get();
 
         /**
-         * {@return the exception thrown by the subtask}
+         * {@return the exception or error thrown by this subtask if it failed}
+         * If the subtask was forked with {@link #fork(Callable) fork(Callable)} then the
+         * exception or error thrown by the {@link Callable#call() call} method is returned.
+         * If the subtask was forked with {@link #fork(Runnable) fork(Runnable)} then the
+         * exception or error thrown by the {@link Runnable#run() run} method is returned.
+         *
+         * <p> Code executing in the scope owner thread can use this method to get the
+         * exception thrown by a failed subtask only after it has {@linkplain #join() joined}.
          *
-         * <p> To ensure correct usage, if the scope owner {@linkplain #fork(Callable) forks}
-         * a subtask, then it must join (with {@link #join() join} or {@link #joinUntil(Instant)
-         * joinUntil}) before it can obtain the exception thrown by the subtask.
+         * <p> Code executing in a {@code Joiner} {@link Joiner#onComplete(Subtask)
+         * onComplete} method should test that the {@linkplain #state() subtask state} is
+         * {@link State#FAILED FAILED} before using this method to get the exception.
          *
          * @throws IllegalStateException if the subtask has not completed, completed with
-         * a result, or the current thread is the task scope owner and did not join after
-         * forking
+         * a result, or the current thread is the scope owner invoking this method
+         * before {@linkplain #join() joining}
          * @see State#FAILED
          */
         Throwable exception();
     }
 
     /**
-     * Creates a structured task scope with the given name and thread factory. The task
-     * scope is optionally named for the purposes of monitoring and management. The thread
-     * factory is used to {@link ThreadFactory#newThread(Runnable) create} threads when
-     * subtasks are {@linkplain #fork(Callable) forked}. The task scope is owned by the
-     * current thread.
-     *
-     * <p> Construction captures the current thread's {@linkplain ScopedValue scoped value}
-     * bindings for inheritance by threads started in the task scope. The
-     * <a href="#TreeStructure">Tree Structure</a> section in the class description details
-     * how parent-child relations are established implicitly for the purpose of inheritance
-     * of scoped value bindings.
-     *
-     * @param name the name of the task scope, can be null
-     * @param factory the thread factory
-     */
-    @SuppressWarnings("this-escape")
-    public StructuredTaskScope(String name, ThreadFactory factory) {
-        this.factory = Objects.requireNonNull(factory, "'factory' is null");
-        if (name == null)
-            name = Objects.toIdentityString(this);
-        this.flock = ThreadFlock.open(name);
-    }
-
-    /**
-     * Creates an unnamed structured task scope that creates virtual threads. The task
-     * scope is owned by the current thread.
-     *
-     * @implSpec This constructor is equivalent to invoking the 2-arg constructor with a
-     * name of {@code null} and a thread factory that creates virtual threads.
-     */
-    public StructuredTaskScope() {
-        this(null, Thread.ofVirtual().factory());
-    }
-
-    private IllegalStateException newIllegalStateExceptionScopeClosed() {
-        return new IllegalStateException("Task scope is closed");
-    }
-
-    private IllegalStateException newIllegalStateExceptionNoJoin() {
-        return new IllegalStateException("Owner did not join after forking subtasks");
-    }
-
-    /**
-     * Throws IllegalStateException if the scope is closed, returning the state if not
-     * closed.
-     */
-    private int ensureOpen() {
-        int s = state;
-        if (s == CLOSED)
-            throw newIllegalStateExceptionScopeClosed();
-        return s;
-    }
-
-    /**
-     * Throws WrongThreadException if the current thread is not the owner.
-     */
-    private void ensureOwner() {
-        if (Thread.currentThread() != flock.owner())
-            throw new WrongThreadException("Current thread not owner");
-    }
-
-    /**
-     * Throws WrongThreadException if the current thread is not the owner
-     * or a thread contained in the tree.
-     */
-    private void ensureOwnerOrContainsThread() {
-        Thread currentThread = Thread.currentThread();
-        if (currentThread != flock.owner() && !flock.containsThread(currentThread))
-            throw new WrongThreadException("Current thread not owner or thread in the tree");
-    }
-
-    /**
-     * Throws IllegalStateException if the current thread is the owner, and the owner did
-     * not join after forking a subtask in the given fork round.
-     */
-    private void ensureJoinedIfOwner(int round) {
-        if (Thread.currentThread() == flock.owner() && (round > lastJoinCompleted)) {
-            throw newIllegalStateExceptionNoJoin();
-        }
-    }
-
-    /**
-     * Ensures that the current thread is the owner of this task scope and that it joined
-     * (with {@link #join()} or {@link #joinUntil(Instant)}) after {@linkplain #fork(Callable)
-     * forking} subtasks.
-     *
-     * @apiNote This method can be used by subclasses that define methods to make available
-     * results, state, or other outcome to code intended to execute after the join method.
-     *
-     * @throws WrongThreadException if the current thread is not the task scope owner
-     * @throws IllegalStateException if the task scope is open and task scope owner did
-     * not join after forking
-     */
-    protected final void ensureOwnerAndJoined() {
-        ensureOwner();
-        if (forkRound > lastJoinCompleted) {
-            throw newIllegalStateExceptionNoJoin();
-        }
-    }
-
-    /**
-     * Invoked by a subtask when it completes successfully or fails in this task scope.
-     * This method is not invoked if a subtask completes after the task scope is
-     * {@linkplain #shutdown() shut down}.
-     *
-     * @implSpec The default implementation throws {@code NullPointerException} if the
-     * subtask is {@code null}. It throws {@link IllegalArgumentException} if the subtask
-     * has not completed.
-     *
-     * @apiNote The {@code handleComplete} method should be thread safe. It may be
-     * invoked by several threads concurrently.
-     *
-     * @param subtask the subtask
-     *
-     * @throws IllegalArgumentException if called with a subtask that has not completed
-     */
-    protected void handleComplete(Subtask<? extends T> subtask) {
-        if (subtask.state() == Subtask.State.UNAVAILABLE)
-            throw new IllegalArgumentException();
-    }
-
-    /**
-     * Starts a new thread in this task scope to execute a value-returning task, thus
-     * creating a <em>subtask</em> of this task scope.
-     *
-     * <p> The value-returning task is provided to this method as a {@link Callable}, the
-     * thread executes the task's {@link Callable#call() call} method. The thread is
-     * created with the task scope's {@link ThreadFactory}. It inherits the current thread's
-     * {@linkplain ScopedValue scoped value} bindings. The bindings must match the bindings
-     * captured when the task scope was created.
-     *
-     * <p> This method returns a {@link Subtask Subtask} to represent the <em>forked
-     * subtask</em>. The {@code Subtask} object can be used to obtain the result when
-     * the subtask completes successfully, or the exception when the subtask fails. To
-     * ensure correct usage, the {@link Subtask#get() get()} and {@link Subtask#exception()
-     * exception()} methods may only be called by the task scope owner after it has waited
-     * for all threads to finish with the {@link #join() join} or {@link #joinUntil(Instant)}
-     * methods. When the subtask completes, the thread invokes the {@link
-     * #handleComplete(Subtask) handleComplete} method to consume the completed subtask.
-     * If the task scope is {@linkplain #shutdown() shut down} before the subtask completes
-     * then the {@code handleComplete} method will not be invoked.
-     *
-     * <p> If this task scope is {@linkplain #shutdown() shutdown} (or in the process of
-     * shutting down) then the subtask will not run and the {@code handleComplete} method
-     * will not be invoked.
-     *
-     * <p> This method may only be invoked by the task scope owner or threads contained
-     * in the task scope.
+     * An object used with a {@link StructuredTaskScope} to handle subtask completion and
+     * produce the result for the scope owner waiting in the {@link #join() join} method
+     * for subtasks to complete.
      *
-     * @implSpec This method may be overridden for customization purposes, wrapping tasks
-     * for example. If overridden, the subclass must invoke {@code super.fork} to start a
-     * new thread in this task scope.
-     *
-     * @param task the value-returning task for the thread to execute
-     * @param <U> the result type
-     * @return the subtask
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner or a
-     * thread contained in the task scope
-     * @throws StructureViolationException if the current scoped value bindings are not
-     * the same as when the task scope was created
-     * @throws RejectedExecutionException if the thread factory rejected creating a
-     * thread to run the subtask
-     */
-    public <U extends T> Subtask<U> fork(Callable<? extends U> task) {
-        Objects.requireNonNull(task, "'task' is null");
-        int s = ensureOpen();   // throws ISE if closed
-
-        // when forked by the owner, the subtask is forked in the current or next round
-        int round = -1;
-        if (Thread.currentThread() == flock.owner()) {
-            round = forkRound;
-            if (forkRound == lastJoinCompleted) {
-                // new round if first fork after join
-                round++;
-            }
-        }
-
-        SubtaskImpl<U> subtask = new SubtaskImpl<>(this, task, round);
-        if (s < SHUTDOWN) {
-            // create thread to run task
-            Thread thread = factory.newThread(subtask);
-            if (thread == null) {
-                throw new RejectedExecutionException("Rejected by thread factory");
-            }
-
-            // attempt to start the thread
-            try {
-                flock.start(thread);
-            } catch (IllegalStateException e) {
-                // shutdown by another thread, or underlying flock is shutdown due
-                // to unstructured use
-            }
-        }
-
-        // force owner to join if this is the first fork in the round
-        if (Thread.currentThread() == flock.owner() && round > forkRound) {
-            forkRound = round;
-        }
-
-        // return forked subtask or a subtask that did not run
-        return subtask;
-    }
-
-    /**
-     * Wait for all threads to finish or the task scope to shut down.
-     */
-    private void implJoin(Duration timeout)
-        throws InterruptedException, TimeoutException
-    {
-        ensureOwner();
-        lastJoinAttempted = forkRound;
-        int s = ensureOpen();  // throws ISE if closed
-        if (s == OPEN) {
-            // wait for all threads, wakeup, interrupt, or timeout
-            if (timeout != null) {
-                flock.awaitAll(timeout);
-            } else {
-                flock.awaitAll();
-            }
-        }
-        lastJoinCompleted = forkRound;
-    }
-
-    /**
-     * Wait for all subtasks started in this task scope to finish or the task scope to
-     * shut down.
-     *
-     * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-     * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-     * when all threads finish, the task scope is {@linkplain #shutdown() shut down}, or
-     * the current thread is {@linkplain Thread#interrupt() interrupted}.
-     *
-     * <p> This method may only be invoked by the task scope owner.
-     *
-     * @implSpec This method may be overridden for customization purposes or to return a
-     * more specific return type. If overridden, the subclass must invoke {@code
-     * super.join} to ensure that the method waits for threads in this task scope to
-     * finish.
-     *
-     * @return this task scope
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner
-     * @throws InterruptedException if interrupted while waiting
-     */
-    public StructuredTaskScope<T> join() throws InterruptedException {
-        try {
-            implJoin(null);
-        } catch (TimeoutException e) {
-            throw new InternalError();
-        }
-        return this;
-    }
-
-    /**
-     * Wait for all subtasks started in this task scope to finish or the task scope to
-     * shut down, up to the given deadline.
-     *
-     * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-     * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-     * when all threads finish, the task scope is {@linkplain #shutdown() shut down}, the
-     * deadline is reached, or the current thread is {@linkplain Thread#interrupt()
-     * interrupted}.
-     *
-     * <p> This method may only be invoked by the task scope owner.
-     *
-     * @implSpec This method may be overridden for customization purposes or to return a
-     * more specific return type. If overridden, the subclass must invoke {@code
-     * super.joinUntil} to ensure that the method waits for threads in this task scope to
-     * finish.
-     *
-     * @param deadline the deadline
-     * @return this task scope
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner
-     * @throws InterruptedException if interrupted while waiting
-     * @throws TimeoutException if the deadline is reached while waiting
-     */
-    public StructuredTaskScope<T> joinUntil(Instant deadline)
-        throws InterruptedException, TimeoutException
-    {
-        Duration timeout = Duration.between(Instant.now(), deadline);
-        implJoin(timeout);
-        return this;
-    }
-
-    /**
-     * Interrupt all unfinished threads.
-     */
-    private void interruptAll() {
-        flock.threads()
-            .filter(t -> t != Thread.currentThread())
-            .forEach(t -> {
-                try {
-                    t.interrupt();
-                } catch (Throwable ignore) { }
-            });
-    }
-
-    /**
-     * Shutdown the task scope if not already shutdown. Return true if this method
-     * shutdowns the task scope, false if already shutdown.
-     */
-    private boolean implShutdown() {
-        shutdownLock.lock();
-        try {
-            if (state < SHUTDOWN) {
-                // prevent new threads from starting
-                flock.shutdown();
-
-                // set status before interrupting tasks
-                state = SHUTDOWN;
-
-                // interrupt all unfinished threads
-                interruptAll();
-
-                return true;
-            } else {
-                // already shutdown
-                return false;
-            }
-        } finally {
-            shutdownLock.unlock();
-        }
-    }
-
-    /**
-     * Shut down this task scope without closing it. Shutting down a task scope prevents
-     * new threads from starting, interrupts all unfinished threads, and causes the
-     * {@link #join() join} method to wakeup. Shutdown is useful for cases where the
-     * results of unfinished subtasks are no longer needed. It will typically be called
-     * by the {@link #handleComplete(Subtask)} implementation of a subclass that
-     * implements a policy to discard unfinished tasks once some outcome is reached.
-     *
-     * <p> More specifically, this method:
+     * <p> Joiner defines static methods to create {@code Joiner} objects for common cases:
      * <ul>
-     * <li> {@linkplain Thread#interrupt() Interrupts} all unfinished threads in the
-     * task scope (except the current thread).
-     * <li> Wakes up the task scope owner if it is waiting in {@link #join()} or {@link
-     * #joinUntil(Instant)}. If the task scope owner is not waiting then its next call to
-     * {@code join} or {@code joinUntil} will return immediately.
+     *   <li> {@link #allSuccessfulOrThrow() allSuccessfulOrThrow()} creates a {@code Joiner}
+     *   that yields a stream of the completed subtasks for {@code join} to return when
+     *   all subtasks complete successfully. It cancels the scope and causes {@code join}
+     *   to throw if any subtask fails.
+     *   <li> {@link #anySuccessfulResultOrThrow() anySuccessfulResultOrThrow()} creates a
+     *   {@code Joiner} that yields the result of the first subtask to succeed for {@code
+     *   join} to return. It causes {@code join} to throw if all subtasks fail.
+     *   <li> {@link #awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()} creates a
+     *   {@code Joiner} that waits for all successful subtasks. It cancels the scope and
+     *   causes {@code join} to throw if any subtask fails.
+     *   <li> {@link #awaitAll() awaitAll()} creates a {@code Joiner} that waits for all
+     *   subtasks. It does not cancel the scope or cause {@code join} to throw.
      * </ul>
      *
-     * <p> The {@linkplain Subtask.State state} of unfinished subtasks that complete at
-     * around the time that the task scope is shutdown is not defined. A subtask that
-     * completes successfully with a result, or fails with an exception, at around
-     * the time that the task scope is shutdown may or may not <i>transition</i> to a
-     * terminal state.
-     *
-     * <p> This method may only be invoked by the task scope owner or threads contained
-     * in the task scope.
-     *
-     * @implSpec This method may be overridden for customization purposes. If overridden,
-     * the subclass must invoke {@code super.shutdown} to ensure that the method shuts
-     * down the task scope.
-     *
-     * @apiNote
-     * There may be threads that have not finished because they are executing code that
-     * did not respond (or respond promptly) to thread interrupt. This method does not wait
-     * for these threads. When the owner invokes the {@link #close() close} method
-     * to close the task scope then it will wait for the remaining threads to finish.
+     * <p> In addition to the methods to create {@code Joiner} objects for common cases,
+     * the {@link #allUntil(Predicate) allUntil(Predicate)} method is defined to create a
+     * {@code Joiner} that yields a stream of all subtasks. It is created with a {@link
+     * Predicate Predicate} that determines if the scope should continue or be cancelled.
+     * This {@code Joiner} can be built upon to create custom policies that cancel the
+     * scope based on some condition.
      *
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner or
-     * a thread contained in the task scope
-     * @see #isShutdown()
-     */
-    public void shutdown() {
-        ensureOwnerOrContainsThread();
-        int s = ensureOpen();  // throws ISE if closed
-        if (s < SHUTDOWN && implShutdown())
-            flock.wakeup();
-    }
-
-    /**
-     * {@return true if this task scope is shutdown, otherwise false}
-     * @see #shutdown()
-     */
-    public final boolean isShutdown() {
-        return state >= SHUTDOWN;
-    }
-
-    /**
-     * Closes this task scope.
+     * <p> More advanced policies can be developed by implementing the {@code Joiner}
+     * interface. The {@link #onFork(Subtask)} method is invoked when subtasks are forked.
+     * The {@link #onComplete(Subtask)} method is invoked when subtasks complete with a
+     * result or exception. These methods return a {@code boolean} to indicate if scope
+     * should be cancelled. These methods can be used to collect subtasks, results, or
+     * exceptions, and control when to cancel the scope. The {@link #result()} method
+     * must be implemented to produce the result (or exception) for the {@code join}
+     * method.
      *
-     * <p> This method first shuts down the task scope (as if by invoking the {@link
-     * #shutdown() shutdown} method). It then waits for the threads executing any
-     * unfinished tasks to finish. If interrupted, this method will continue to wait for
-     * the threads to finish before completing with the interrupt status set.
+     * <p> Unless otherwise specified, passing a {@code null} argument to a method
+     * in this class will cause a {@link NullPointerException} to be thrown.
      *
-     * <p> This method may only be invoked by the task scope owner. If the task scope
-     * is already closed then the task scope owner invoking this method has no effect.
+     * @implSpec Implementations of this interface must be thread safe. The {@link
+     * #onComplete(Subtask)} method defined by this interface may be invoked by several
+     * threads concurrently.
      *
-     * <p> A {@code StructuredTaskScope} is intended to be used in a <em>structured
-     * manner</em>. If this method is called to close a task scope before nested task
-     * scopes are closed then it closes the underlying construct of each nested task scope
-     * (in the reverse order that they were created in), closes this task scope, and then
-     * throws {@link StructureViolationException}.
-     * Similarly, if this method is called to close a task scope while executing with
-     * {@linkplain ScopedValue scoped value} bindings, and the task scope was created
-     * before the scoped values were bound, then {@code StructureViolationException} is
-     * thrown after closing the task scope.
-     * If a thread terminates without first closing task scopes that it owns then
-     * termination will cause the underlying construct of each of its open tasks scopes to
-     * be closed. Closing is performed in the reverse order that the task scopes were
-     * created in. Thread termination may therefore be delayed when the task scope owner
-     * has to wait for threads forked in these task scopes to finish.
+     * @apiNote It is very important that a new {@code Joiner} object is created for each
+     * {@code StructuredTaskScope}. {@code Joiner} objects should never be shared with
+     * different scopes or re-used after a task is closed.
      *
-     * @implSpec This method may be overridden for customization purposes. If overridden,
-     * the subclass must invoke {@code super.close} to close the task scope.
+     * <p> Designing a {@code Joiner} should take into account the code at the use-site
+     * where the results from the {@link StructuredTaskScope#join() join} method are
+     * processed. It should be clear what the {@code Joiner} does vs. the application
+     * code at the use-site. In general, the {@code Joiner} implementation is not the
+     * place to code "business logic". A {@code Joiner} should be designed to be as
+     * general purpose as possible.
      *
-     * @throws IllegalStateException thrown after closing the task scope if the task scope
-     * owner did not attempt to join after forking
-     * @throws WrongThreadException if the current thread is not the task scope owner
-     * @throws StructureViolationException if a structure violation was detected
-     */
-    @Override
-    public void close() {
-        ensureOwner();
-        int s = state;
-        if (s == CLOSED)
-            return;
-
-        try {
-            if (s < SHUTDOWN)
-                implShutdown();
-            flock.close();
-        } finally {
-            state = CLOSED;
-        }
-
-        // throw ISE if the owner didn't attempt to join after forking
-        if (forkRound > lastJoinAttempted) {
-            lastJoinCompleted = forkRound;
-            throw newIllegalStateExceptionNoJoin();
-        }
-    }
-
-    @Override
-    public String toString() {
-        String name = flock.name();
-        return switch (state) {
-            case OPEN     -> name;
-            case SHUTDOWN -> name + "/shutdown";
-            case CLOSED   -> name + "/closed";
-            default -> throw new InternalError();
-        };
-    }
-
-    /**
-     * Subtask implementation, runs the task specified to the fork method.
+     * @param <T> the result type of subtasks executed in the scope
+     * @param <R> the result type of the scope
+     * @since 24
+     * @see #open(Joiner)
      */
-    private static final class SubtaskImpl<T> implements Subtask<T>, Runnable {
-        private static final AltResult RESULT_NULL = new AltResult(Subtask.State.SUCCESS);
-
-        private record AltResult(Subtask.State state, Throwable exception) {
-            AltResult(Subtask.State state) {
-                this(state, null);
-            }
-        }
-
-        private final StructuredTaskScope<? super T> scope;
-        private final Callable<? extends T> task;
-        private final int round;
-        private volatile Object result;
-
-        SubtaskImpl(StructuredTaskScope<? super T> scope,
-                    Callable<? extends T> task,
-                    int round) {
-            this.scope = scope;
-            this.task = task;
-            this.round = round;
-        }
-
-        @Override
-        public void run() {
-            T result = null;
-            Throwable ex = null;
-            try {
-                result = task.call();
-            } catch (Throwable e) {
-                ex = e;
-            }
-
-            // nothing to do if task scope is shutdown
-            if (scope.isShutdown())
-                return;
-
-            // capture result or exception, invoke handleComplete
-            if (ex == null) {
-                this.result = (result != null) ? result : RESULT_NULL;
-            } else {
-                this.result = new AltResult(State.FAILED, ex);
-            }
-            scope.handleComplete(this);
-        }
-
-        @Override
-        public Callable<? extends T> task() {
-            return task;
-        }
-
-        @Override
-        public Subtask.State state() {
-            Object result = this.result;
-            if (result == null) {
-                return State.UNAVAILABLE;
-            } else if (result instanceof AltResult alt) {
-                // null or failed
-                return alt.state();
-            } else {
-                return State.SUCCESS;
-            }
-        }
-
-        @Override
-        public T get() {
-            scope.ensureJoinedIfOwner(round);
-            Object result = this.result;
-            if (result instanceof AltResult) {
-                if (result == RESULT_NULL) return null;
-            } else if (result != null) {
-                @SuppressWarnings("unchecked")
-                T r = (T) result;
-                return r;
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    @FunctionalInterface
+    interface Joiner<T, R> {
+        /**
+         * Invoked by {@link #fork(Callable) fork(Callable)} and {@link #fork(Runnable)
+         * fork(Runnable)} when forking a subtask. The method is invoked from the task
+         * owner thread. The method is invoked before a thread is created to run the
+         * subtask.
+         *
+         * @implSpec The default implementation throws {@code NullPointerException} if the
+         * subtask is {@code null}. It throws {@code IllegalArgumentException} if the
+         * subtask is not in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state, it
+         * otherwise returns {@code false}.
+         *
+         * @apiNote This method is invoked by the {@code fork} methods. It should not be
+         * invoked directly.
+         *
+         * @param subtask the subtask
+         * @return {@code true} to cancel the scope, otherwise {@code false}
+         */
+        default boolean onFork(Subtask<? extends T> subtask) {
+            if (subtask.state() != Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
             }
-            throw new IllegalStateException(
-                    "Result is unavailable or subtask did not complete successfully");
+            return false;
         }
 
-        @Override
-        public Throwable exception() {
-            scope.ensureJoinedIfOwner(round);
-            Object result = this.result;
-            if (result instanceof AltResult alt && alt.state() == State.FAILED) {
-                return alt.exception();
+        /**
+         * Invoked by the thread started to execute a subtask after the subtask completes
+         * successfully or fails with an exception. This method is not invoked if a
+         * subtask completes after the scope is cancelled.
+         *
+         * @implSpec The default implementation throws {@code NullPointerException} if the
+         * subtask is {@code null}. It throws {@code IllegalArgumentException} if the
+         * subtask is not in the {@link Subtask.State#SUCCESS SUCCESS} or {@link
+         * Subtask.State#FAILED FAILED} state, it otherwise returns {@code false}.
+         *
+         * @apiNote This method is invoked by subtasks when they complete. It should not
+         * be invoked directly.
+         *
+         * @param subtask the subtask
+         * @return {@code true} to cancel the scope, otherwise {@code false}
+         */
+        default boolean onComplete(Subtask<? extends T> subtask) {
+            if (subtask.state() == Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
             }
-            throw new IllegalStateException(
-                    "Exception is unavailable or subtask did not complete with exception");
-        }
-
-        @Override
-        public String toString() {
-            String stateAsString = switch (state()) {
-                case UNAVAILABLE -> "[Unavailable]";
-                case SUCCESS     -> "[Completed successfully]";
-                case FAILED      -> {
-                    Throwable ex = ((AltResult) result).exception();
-                    yield "[Failed: " + ex + "]";
-                }
-            };
-            return Objects.toIdentityString(this) + stateAsString;
-        }
-    }
-
-    /**
-     * A {@code StructuredTaskScope} that captures the result of the first subtask to
-     * complete {@linkplain Subtask.State#SUCCESS successfully}. Once captured, it
-     * {@linkplain #shutdown() shuts down} the task scope to interrupt unfinished threads
-     * and wakeup the task scope owner. The policy implemented by this class is intended
-     * for cases where the result of any subtask will do ("invoke any") and where the
-     * results of other unfinished subtasks are no longer needed.
-     *
-     * <p> Unless otherwise specified, passing a {@code null} argument to a method
-     * in this class will cause a {@link NullPointerException} to be thrown.
-     *
-     * @apiNote This class implements a policy to shut down the task scope when a subtask
-     * completes successfully. There shouldn't be any need to directly shut down the task
-     * scope with the {@link #shutdown() shutdown} method.
-     *
-     * @param <T> the result type
-     * @since 21
-     */
-    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-    public static final class ShutdownOnSuccess<T> extends StructuredTaskScope<T> {
-        private static final Object RESULT_NULL = new Object();
-        private static final VarHandle FIRST_RESULT;
-        private static final VarHandle FIRST_EXCEPTION;
-        static {
-            MethodHandles.Lookup l = MethodHandles.lookup();
-            FIRST_RESULT = MhUtil.findVarHandle(l, "firstResult", Object.class);
-            FIRST_EXCEPTION = MhUtil.findVarHandle(l, "firstException", Throwable.class);
+            return false;
         }
-        private volatile Object firstResult;
-        private volatile Throwable firstException;
 
         /**
-         * Constructs a new {@code ShutdownOnSuccess} with the given name and thread factory.
-         * The task scope is optionally named for the purposes of monitoring and management.
-         * The thread factory is used to {@link ThreadFactory#newThread(Runnable) create}
-         * threads when subtasks are {@linkplain #fork(Callable) forked}. The task scope
-         * is owned by the current thread.
+         * Invoked by the {@link #join() join()} method to produce the result (or exception)
+         * after waiting for all subtasks to complete or the scope cancelled. The result
+         * from this method is returned by the {@code join} method. If this method throws,
+         * then {@code join} throws {@link FailedException} with the exception thrown by
+         * this method as the cause.
          *
-         * <p> Construction captures the current thread's {@linkplain ScopedValue scoped
-         * value} bindings for inheritance by threads started in the task scope. The
-         * {@linkplain StructuredTaskScope##TreeStructure Tree Structure} section
-         * in the class description details how parent-child relations are established
-         * implicitly for the purpose of inheritance of scoped value bindings.
+         * <p> In normal usage, this method will be called at most once by the {@code join}
+         * method to produce the result (or exception). The behavior of this method when
+         * invoked directly, and invoked more than once, is not specified. Where possible,
+         * an implementation should return an equal result (or throw the same exception)
+         * on second or subsequent calls to produce the outcome.
          *
-         * @param name the name of the task scope, can be null
-         * @param factory the thread factory
+         * @apiNote This method is invoked by the {@code join} method. It should not be
+         * invoked directly.
+         *
+         * @return the result
+         * @throws Throwable the exception
          */
-        public ShutdownOnSuccess(String name, ThreadFactory factory) {
-            super(name, factory);
-        }
+        R result() throws Throwable;
 
         /**
-         * Constructs a new unnamed {@code ShutdownOnSuccess} that creates virtual threads.
+         * {@return a new Joiner object that yields a stream of all subtasks when all
+         * subtasks complete successfully}
+         * The {@code Joiner} <a href="StructuredTaskScope.html#Cancallation">cancels</a>
+         * the scope and causes {@code join} to throw if any subtask fails.
+         *
+         * <p> If all subtasks complete successfully, the joiner's {@link Joiner#result()}
+         * method returns a stream of all subtasks in the order that they were forked.
+         * If any subtask failed then the {@code result} method throws the exception from
+         * the first subtask to fail.
          *
-         * @implSpec This constructor is equivalent to invoking the 2-arg constructor with
-         * a name of {@code null} and a thread factory that creates virtual threads.
+         * @apiNote Joiners returned by this method are suited to cases where all subtasks
+         * return a result of the same type. Joiners returned by {@link
+         * #awaitAllSuccessfulOrThrow()} are suited to cases where the subtasks return
+         * results of different types.
+         *
+         * @param <T> the result type of subtasks
          */
-        public ShutdownOnSuccess() {
-            this(null, Thread.ofVirtual().factory());
-        }
-
-        @Override
-        protected void handleComplete(Subtask<? extends T> subtask) {
-            if (firstResult != null) {
-                // already captured a result
-                return;
-            }
-
-            if (subtask.state() == Subtask.State.SUCCESS) {
-                // task succeeded
-                T result = subtask.get();
-                Object r = (result != null) ? result : RESULT_NULL;
-                if (FIRST_RESULT.compareAndSet(this, null, r)) {
-                    super.shutdown();
-                }
-            } else if (firstException == null) {
-                // capture the exception thrown by the first subtask that failed
-                FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
-            }
+        static <T> Joiner<T, Stream<Subtask<T>>> allSuccessfulOrThrow() {
+            return new Joiners.AllSuccessful<>();
         }
 
         /**
-         * Wait for a subtask started in this task scope to complete {@linkplain
-         * Subtask.State#SUCCESS successfully} or all subtasks to complete.
-         *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask completes successfully, or the current
-         * thread is {@linkplain Thread#interrupt() interrupted}. It also stops waiting
-         * if the {@link #shutdown() shutdown} method is invoked directly to shut down
-         * this task scope.
+         * {@return a new Joiner object that yields the result of any subtask that
+         * completed successfully}
+         * The {@code Joiner} causes {@code join} to throw if all subtasks fail.
          *
-         * <p> This method may only be invoked by the task scope owner.
+         * <p> The joiner's {@link Joiner#result()} method returns the result of a subtask
+         * that completed successfully. If all subtasks fail then the {@code result} method
+         * throws the exception from one of the failed subtasks. The {@code result} method
+         * throws {@code NoSuchElementException} if no subtasks were forked.
          *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
+         * @param <T> the result type of subtasks
          */
-        @Override
-        public ShutdownOnSuccess<T> join() throws InterruptedException {
-            super.join();
-            return this;
+        static <T> Joiner<T, T> anySuccessfulResultOrThrow() {
+            return new Joiners.AnySuccessful<>();
         }
 
         /**
-         * Wait for a subtask started in this task scope to complete {@linkplain
-         * Subtask.State#SUCCESS successfully} or all subtasks to complete, up to the
-         * given deadline.
+         * {@return a new Joiner object that waits for subtasks to complete successfully}
+         * The {@code Joiner} <a href="StructuredTaskScope.html#Cancallation">cancels</a>
+         * the scope and causes {@code join} to throw if any subtask fails.
          *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask completes successfully, the deadline is
-         * reached, or the current thread is {@linkplain Thread#interrupt() interrupted}.
-         * It also stops waiting if the {@link #shutdown() shutdown} method is invoked
-         * directly to shut down this task scope.
+         * <p> The joiner's {@link Joiner#result() result} method returns {@code null}
+         * if all subtasks complete successfully, or throws the exception from the first
+         * subtask to fail.
          *
-         * <p> This method may only be invoked by the task scope owner.
+         * @apiNote Joiners returned by this method are suited to cases where subtasks
+         * return results of different types. Joiners returned by {@link #allSuccessfulOrThrow()}
+         * are suited to cases where the subtasks return a result of the same type.
          *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
+         * @param <T> the result type of subtasks
          */
-        @Override
-        public ShutdownOnSuccess<T> joinUntil(Instant deadline)
-            throws InterruptedException, TimeoutException
-        {
-            super.joinUntil(deadline);
-            return this;
+        static <T> Joiner<T, Void> awaitAllSuccessfulOrThrow() {
+            return new Joiners.AwaitSuccessful<>();
         }
 
         /**
-         * {@return the result of the first subtask that completed {@linkplain
-         * Subtask.State#SUCCESS successfully}}
+         * {@return a new Joiner object that waits for all subtasks to complete}
+         * The {@code Joiner} does not cancel the scope if a subtask fails.
+         *
+         * <p> The joiner's {@link Joiner#result() result} method returns {@code null}.
          *
-         * <p> When no subtask completed successfully, but a subtask {@linkplain
-         * Subtask.State#FAILED failed} then {@code ExecutionException} is thrown with
-         * the subtask's exception as the {@linkplain Throwable#getCause() cause}.
+         * @apiNote This Joiner is useful for cases where subtasks make use of
+         * <em>side-effects</em> rather than return results or fail with exceptions.
+         * The {@link #fork(Runnable) fork(Runnable)} method can be used to fork subtasks
+         * that do not return a result.
          *
-         * @throws ExecutionException if no subtasks completed successfully but at least
-         * one subtask failed
-         * @throws IllegalStateException if no subtasks completed or the task scope owner
-         * did not join after forking
-         * @throws WrongThreadException if the current thread is not the task scope owner
+         * <p> This Joiner can also be used for <em>fan-in</em> scenarios where subtasks
+         * are forked to handle incoming connections and the number of subtasks is unbounded.
+         * In this example, the thread executing the {@code acceptLoop} method will only
+         * stop when interrupted or the listener socket is closed asynchronously.
+         * {@snippet lang=java :
+         *   void acceptLoop(ServerSocket listener) throws IOException, InterruptedException {
+         *       try (var scope = StructuredTaskScope.open(Joiner.<Socket>awaitAll())) {
+         *           while (true) {
+         *               Socket socket = listener.accept();
+         *               scope.fork(() -> handle(socket));
+         *           }
+         *       }
+         *   }
+         * }
+         *
+         * @param <T> the result type of subtasks
          */
-        public T result() throws ExecutionException {
-            return result(ExecutionException::new);
+        static <T> Joiner<T, Void> awaitAll() {
+            // ensure that new Joiner object is returned
+            return new Joiner<T, Void>() {
+                @Override
+                public Void result() {
+                    return null;
+                }
+            };
         }
 
         /**
-         * Returns the result of the first subtask that completed {@linkplain
-         * Subtask.State#SUCCESS successfully}, otherwise throws an exception produced
-         * by the given exception supplying function.
+         * {@return a new Joiner object that yields a stream of all subtasks when all
+         * subtasks complete or a predicate returns {@code true} to cancel the scope}
+         *
+         * <p> The joiner's {@link Joiner#onComplete(Subtask)} method invokes the
+         * predicate's {@link Predicate#test(Object) test} method with the subtask that
+         * completed successfully or failed with an exception. If the {@code test} method
+         * returns {@code true} then <a href="StructuredTaskScope.html#Cancallation">
+         * the scope is cancelled</a>. The {@code test} method must be thread safe as it
+         * may be invoked concurrently from several threads. If the {@code test} method
+         * completes with an exception or error, then the thread that executed the subtask
+         * invokes the {@linkplain Thread.UncaughtExceptionHandler uncaught exception handler}
+         * with the exception or error before the thread terminates.
+         *
+         * <p> The joiner's {@link #result()} method returns the stream of all subtasks,
+         * in fork order. The stream may contain subtasks that have completed
+         * (in {@link Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED}
+         * state) or subtasks in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state
+         * if the scope was cancelled before all subtasks were forked or completed.
+         *
+         * <p> The following example uses this method to create a {@code Joiner} that
+         * <a href="StructuredTaskScope.html#Cancallation">cancels</a> the scope when
+         * two or more subtasks fail.
+         * {@snippet lang=java :
+         *    class CancelAfterTwoFailures<T> implements Predicate<Subtask<? extends T>> {
+         *         private final AtomicInteger failedCount = new AtomicInteger();
+         *         @Override
+         *         public boolean test(Subtask<? extends T> subtask) {
+         *             return subtask.state() == Subtask.State.FAILED
+         *                     && failedCount.incrementAndGet() >= 2;
+         *         }
+         *     }
          *
-         * <p> When no subtask completed successfully, but a subtask {@linkplain
-         * Subtask.State#FAILED failed}, then the exception supplying function is invoked
-         * with subtask's exception.
+         *     var joiner = Joiner.all(new CancelAfterTwoFailures<String>());
+         * }
          *
-         * @param esf the exception supplying function
-         * @param <X> type of the exception to be thrown
-         * @return the result of the first subtask that completed with a result
+         * <p> The following example uses {@code allUntil} to wait for all subtasks to
+         * complete without any cancellation. This is similar to {@link #awaitAll()}
+         * except that it yields a stream of the completed subtasks.
+         * {@snippet lang=java :
+         *    <T> List<Subtask<T>> invokeAll(Collection<Callable<T>> tasks) throws InterruptedException {
+         *        try (var scope = StructuredTaskScope.open(Joiner.<T>allUntil(_ -> false))) {
+         *            tasks.forEach(scope::fork);
+         *            return scope.join().toList();
+         *        }
+         *    }
+         * }
          *
-         * @throws X if no subtasks completed successfully but at least one subtask failed
-         * @throws IllegalStateException if no subtasks completed or the task scope owner
-         * did not join after forking
-         * @throws WrongThreadException if the current thread is not the task scope owner
+         * @param isDone the predicate to evaluate completed subtasks
+         * @param <T> the result type of subtasks
          */
-        public <X extends Throwable> T result(Function<Throwable, ? extends X> esf) throws X {
-            Objects.requireNonNull(esf);
-            ensureOwnerAndJoined();
-
-            Object result = firstResult;
-            if (result == RESULT_NULL) {
-                return null;
-            } else if (result != null) {
-                @SuppressWarnings("unchecked")
-                T r = (T) result;
-                return r;
-            }
-
-            Throwable exception = firstException;
-            if (exception != null) {
-                X ex = esf.apply(exception);
-                Objects.requireNonNull(ex, "esf returned null");
-                throw ex;
-            }
-
-            throw new IllegalStateException("No completed subtasks");
+        static <T> Joiner<T, Stream<Subtask<T>>> allUntil(Predicate<Subtask<? extends T>> isDone) {
+            return new Joiners.AllSubtasks<>(isDone);
         }
     }
 
     /**
-     * A {@code StructuredTaskScope} that captures the exception of the first subtask to
-     * {@linkplain Subtask.State#FAILED fail}. Once captured, it {@linkplain #shutdown()
-     * shuts down} the task scope to interrupt unfinished threads and wakeup the task
-     * scope owner. The policy implemented by this class is intended for cases where the
-     * results for all subtasks are required ("invoke all"); if any subtask fails then the
-     * results of other unfinished subtasks are no longer needed.
+     * Represents the configuration for a {@code StructuredTaskScope}.
+     *
+     * <p> The configuration for a {@code StructuredTaskScope} consists of a {@link
+     * ThreadFactory} to create threads, an optional name for the purposes of monitoring
+     * and management, and an optional timeout.
+     *
+     * <p> Creating a {@code StructuredTaskScope} with {@link #open()} or {@link #open(Joiner)}
+     * uses the <a href="StructuredTaskScope.html#DefaultConfiguration">default
+     * configuration</a>. The default configuration consists of a thread factory that
+     * creates unnamed <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">
+     * virtual threads</a>, no name for monitoring and management purposes, and no timeout.
+     *
+     * <p> Creating a {@code StructuredTaskScope} with its 2-arg {@link #open(Joiner, Function)
+     * open} method allows a different configuration to be used. The function specified
+     * to the {@code open} method is applied to the default configuration and returns the
+     * configuration for the {@code StructuredTaskScope} under construction. The function
+     * can use the {@code with-} prefixed methods defined here to specify the components
+     * of the configuration to use.
      *
      * <p> Unless otherwise specified, passing a {@code null} argument to a method
      * in this class will cause a {@link NullPointerException} to be thrown.
      *
-     * @apiNote This class implements a policy to shut down the task scope when a subtask
-     * fails. There shouldn't be any need to directly shut down the task scope with the
-     * {@link #shutdown() shutdown} method.
-     *
-     * @since 21
+     * @since 24
      */
     @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-    public static final class ShutdownOnFailure extends StructuredTaskScope<Object> {
-        private static final VarHandle FIRST_EXCEPTION =
-                MhUtil.findVarHandle(MethodHandles.lookup(), "firstException", Throwable.class);
-        private volatile Throwable firstException;
-
+    sealed interface Config permits StructuredTaskScopeImpl.ConfigImpl {
         /**
-         * Constructs a new {@code ShutdownOnFailure} with the given name and thread factory.
-         * The task scope is optionally named for the purposes of monitoring and management.
-         * The thread factory is used to {@link ThreadFactory#newThread(Runnable) create}
-         * threads when subtasks are {@linkplain #fork(Callable) forked}. The task scope
-         * is owned by the current thread.
+         * {@return a new {@code Config} object with the given thread factory}
+         * The other components are the same as this object. The thread factory is used by
+         * a scope to create threads when {@linkplain #fork(Callable) forking} subtasks.
+         * @param threadFactory the thread factory
          *
-         * <p> Construction captures the current thread's {@linkplain ScopedValue scoped
-         * value} bindings for inheritance by threads started in the task scope. The
-         * {@linkplain StructuredTaskScope##TreeStructure Tree Structure} section in the class description
-         * details how parent-child relations are established implicitly for the purpose
-         * of inheritance of scoped value bindings.
+         * @apiNote The thread factory will typically create
+         * <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual threads</a>,
+         * maybe with names for monitoring purposes, an {@linkplain Thread.UncaughtExceptionHandler
+         * uncaught exception handler}, or other properties configured.
          *
-         * @param name the name of the task scope, can be null
-         * @param factory the thread factory
+         * @see #fork(Callable)
          */
-        public ShutdownOnFailure(String name, ThreadFactory factory) {
-            super(name, factory);
-        }
+        Config withThreadFactory(ThreadFactory threadFactory);
 
         /**
-         * Constructs a new unnamed {@code ShutdownOnFailure} that creates virtual threads.
-         *
-         * @implSpec This constructor is equivalent to invoking the 2-arg constructor with
-         * a name of {@code null} and a thread factory that creates virtual threads.
+         * {@return a new {@code Config} object with the given name}
+         * The other components are the same as this object. A scope is optionally
+         * named for the purposes of monitoring and management.
+         * @param name the name
          */
-        public ShutdownOnFailure() {
-            this(null, Thread.ofVirtual().factory());
-        }
-
-        @Override
-        protected void handleComplete(Subtask<?> subtask) {
-            if (subtask.state() == Subtask.State.FAILED
-                    && firstException == null
-                    && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception())) {
-                super.shutdown();
-            }
-        }
+        Config withName(String name);
 
         /**
-         * Wait for all subtasks started in this task scope to complete or for a subtask
-         * to {@linkplain Subtask.State#FAILED fail}.
+         * {@return a new {@code Config} object with the given timeout}
+         * The other components are the same as this object.
+         * @param timeout the timeout
          *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask fails, or the current thread is {@linkplain
-         * Thread#interrupt() interrupted}. It also stops waiting if the {@link #shutdown()
-         * shutdown} method is invoked directly to shut down this task scope.
+         * @apiNote Applications using deadlines, expressed as an {@link java.time.Instant},
+         * can use {@link Duration#between Duration.between(Instant.now(), deadline)} to
+         * compute the timeout for this method.
          *
-         * <p> This method may only be invoked by the task scope owner.
-         *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
+         * @see #join()
          */
-        @Override
-        public ShutdownOnFailure join() throws InterruptedException {
-            super.join();
-            return this;
-        }
+        Config withTimeout(Duration timeout);
+    }
 
-        /**
-         * Wait for all subtasks started in this task scope to complete or for a subtask
-         * to {@linkplain Subtask.State#FAILED fail}, up to the given deadline.
-         *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask fails, the deadline is reached, or the current
-         * thread is {@linkplain Thread#interrupt() interrupted}. It also stops waiting
-         * if the {@link #shutdown() shutdown} method is invoked directly to shut down
-         * this task scope.
-         *
-         * <p> This method may only be invoked by the task scope owner.
-         *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
-         */
-        @Override
-        public ShutdownOnFailure joinUntil(Instant deadline)
-            throws InterruptedException, TimeoutException
-        {
-            super.joinUntil(deadline);
-            return this;
-        }
+    /**
+     * Exception thrown by {@link #join()} when the outcome is an exception rather than a
+     * result.
+     *
+     * @since 24
+     */
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    final class FailedException extends RuntimeException {
+        @java.io.Serial
+        static final long serialVersionUID = -1533055100078459923L;
 
         /**
-         * Returns the exception of the first subtask that {@linkplain Subtask.State#FAILED
-         * failed}. If no subtasks failed then an empty {@code Optional} is returned.
-         *
-         * @return the exception for the first subtask to fail or an empty optional if no
-         * subtasks failed
+         * Constructs a {@code FailedException} with the specified cause.
          *
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         * @throws IllegalStateException if the task scope owner did not join after forking
+         * @param  cause the cause, can be {@code null}
          */
-        public Optional<Throwable> exception() {
-            ensureOwnerAndJoined();
-            return Optional.ofNullable(firstException);
+        public FailedException(Throwable cause) {
+            super(cause);
         }
+    }
 
-        /**
-         * Throws if a subtask {@linkplain Subtask.State#FAILED failed}.
-         * If any subtask failed with an exception then {@code ExecutionException} is
-         * thrown with the exception of the first subtask to fail as the {@linkplain
-         * Throwable#getCause() cause}. This method does nothing if no subtasks failed.
-         *
-         * @throws ExecutionException if a subtask failed
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         * @throws IllegalStateException if the task scope owner did not join after forking
-         */
-        public void throwIfFailed() throws ExecutionException {
-            throwIfFailed(ExecutionException::new);
-        }
+    /**
+     * Exception thrown by {@link #join()} if the scope was created with a timeout and
+     * the timeout expired before or while waiting in {@code join}.
+     *
+     * @since 24
+     * @see Config#withTimeout(Duration)
+     */
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    final class TimeoutException extends RuntimeException {
+        @java.io.Serial
+        static final long serialVersionUID = 705788143955048766L;
 
         /**
-         * Throws the exception produced by the given exception supplying function if a
-         * subtask {@linkplain Subtask.State#FAILED failed}. If any subtask failed with
-         * an exception then the function is invoked with the exception of the first
-         * subtask to fail. The exception returned by the function is thrown. This method
-         * does nothing if no subtasks failed.
-         *
-         * @param esf the exception supplying function
-         * @param <X> type of the exception to be thrown
-         *
-         * @throws X produced by the exception supplying function
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         * @throws IllegalStateException if the task scope owner did not join after forking
+         * Constructs a {@code TimeoutException} with no detail message.
          */
-        public <X extends Throwable>
-        void throwIfFailed(Function<Throwable, ? extends X> esf) throws X {
-            ensureOwnerAndJoined();
-            Objects.requireNonNull(esf);
-            Throwable exception = firstException;
-            if (exception != null) {
-                X ex = esf.apply(exception);
-                Objects.requireNonNull(ex, "esf returned null");
-                throw ex;
-            }
-        }
+        public TimeoutException() { }
+    }
+
+    /**
+     * Opens a new {@code StructuredTaskScope} to use the given {@code Joiner} object and
+     * with configuration that is the result of applying the given function to the
+     * <a href="#DefaultConfiguration">default configuration</a>.
+     *
+     * <p> The {@code configFunction} is called with the default configuration and returns
+     * the configuration for the new scope. The function may, for example, set the
+     * {@linkplain Config#withThreadFactory(ThreadFactory) ThreadFactory} or set a
+     * {@linkplain Config#withTimeout(Duration) timeout}. If the function completes with
+     * an exception or error then it is propagated by this method. If the function returns
+     * {@code null} then {@code NullPointerException} is thrown.
+     *
+     * <p> If a {@code ThreadFactory} is set then its {@link ThreadFactory#newThread(Runnable)
+     * newThread} method will be called to create threads when {@linkplain #fork(Callable)
+     * forking} subtasks in this scope. If a {@code ThreadFactory} is not set then
+     * forking subtasks will create an unnamed virtual thread for each subtask.
+     *
+     * <p> If a {@linkplain Config#withTimeout(Duration) timeout} is set then it starts
+     * when the scope is opened. If the timeout expires before the scope has
+     * {@linkplain #join() joined} then the scope is <a href="#Cancallation">cancelled</a>
+     * and the {@code join} method throws {@link TimeoutException}.
+     *
+     * <p> The new scope is owned by the current thread. Only code executing in this
+     * thread can {@linkplain #fork(Callable) fork}, {@linkplain #join() join}, or
+     * {@linkplain #close close} the scope.
+     *
+     * <p> Construction captures the current thread's {@linkplain ScopedValue scoped
+     * value} bindings for inheritance by threads started in the scope.
+     *
+     * @param joiner the joiner
+     * @param configFunction a function to produce the configuration
+     * @return a new scope
+     * @param <T> the result type of subtasks executed in the scope
+     * @param <R> the result type of the scope
+     * @since 24
+     */
+    static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner,
+                                                 Function<Config, Config> configFunction) {
+        return StructuredTaskScopeImpl.open(joiner, configFunction);
+    }
+
+    /**
+     * Opens a new {@code StructuredTaskScope}to use the given {@code Joiner} object. The
+     * scope is created with the <a href="#DefaultConfiguration">default configuration</a>.
+     * The default configuration has a {@code ThreadFactory} that creates unnamed
+     * <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual threads</a>,
+     * is unnamed for monitoring and management purposes, and has no timeout.
+     *
+     * @implSpec
+     * This factory method is equivalent to invoking the 2-arg open method with the given
+     * joiner and the {@linkplain Function#identity() identity function}.
+     *
+     * @param joiner the joiner
+     * @return a new scope
+     * @param <T> the result type of subtasks executed in the scope
+     * @param <R> the result type of the scope
+     * @since 24
+     */
+    static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner) {
+        return open(joiner, Function.identity());
     }
-}
+
+    /**
+     * Opens a new {@code StructuredTaskScope} that can be used to fork subtasks that return
+     * results of any type. The scope's {@link #join()} method waits for all subtasks to
+     * succeed or any subtask to fail.
+     *
+     * <p> The {@code join} method returns {@code null} if all subtasks complete successfully.
+     * It throws {@link FailedException} if any subtask fails, with the exception from
+     * the first subtask to fail as the cause.
+     *
+     * <p> The scope is created with the <a href="#DefaultConfiguration">default
+     * configuration</a>. The default configuration has a {@code ThreadFactory} that creates
+     * unnamed <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual
+     * threads</a>, is unnamed for monitoring and management purposes, and has no timeout.
+     *
+     * @implSpec
+     * This factory method is equivalent to invoking the 2-arg open method with a joiner
+     * created with {@link Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()}
+     * and the {@linkplain Function#identity() identity function}.
+     *
+     * @param <T> the result type of subtasks
+     * @return a new scope
+     * @since 24
+     */
+    static <T> StructuredTaskScope<T, Void> open() {
+        return open(Joiner.awaitAllSuccessfulOrThrow(), Function.identity());
+    }
+
+    /**
+     * Fork a subtask by starting a new thread in this scope to execute a value-returning
+     * method. The new thread executes the subtask concurrently with the current thread.
+     * The parameter to this method is a {@link Callable}, the new thread executes its
+     * {@link Callable#call() call()} method.
+     *
+     * <p> This method first creates a {@link Subtask Subtask} object to represent the
+     * <em>forked subtask</em>. It invokes the joiner's {@link Joiner#onFork(Subtask) onFork}
+     * method with the subtask in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state.
+     * If the {@code onFork} completes with an exception or error then it is propagated by
+     * the {@code fork} method without creating a thread. If the scope is already
+     * <a href="#Cancallation">cancelled</a>, or {@code onFork} returns {@code true} to
+     * cancel the scope, then this method returns the {@code Subtask}, in the
+     * {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state, without creating a thread to
+     * execute the subtask.
+     *
+     * <p> If the scope is not cancelled, and the {@code onFork} method returns {@code false},
+     * then a thread is created with the {@link ThreadFactory} configured when the scope
+     * was opened, and the thread is started. Forking a subtask inherits the current thread's
+     * {@linkplain ScopedValue scoped value} bindings. The bindings must match the bindings
+     * captured when the scope was opened. If the subtask completes (successfully or with
+     * an exception) before the scope is cancelled, then the thread invokes the joiner's
+     * {@link Joiner#onComplete(Subtask) onComplete} method with the subtask in the
+     * {@link Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED} state.
+     * If the {@code onComplete} method completes with an exception or error, then the
+     * {@linkplain Thread.UncaughtExceptionHandler uncaught exception handler} is invoked
+     * with the exception or error before the thread terminates.
+     *
+     * <p> This method returns the {@link Subtask Subtask} object. In some usages, this
+     * object may be used to get its result. In other cases it may be used for correlation
+     * or just discarded. To ensure correct usage, the {@link Subtask#get() Subtask.get()}
+     * method may only be called by the scope owner to get the result after it has
+     * waited for subtasks to complete with the {@link #join() join} method and the subtask
+     * completed successfully. Similarly, the {@link Subtask#exception() Subtask.exception()}
+     * method may only be called by the scope owner after it has joined and the subtask
+     * failed. If the scope was cancelled before the subtask was forked, or before it
+     * completes, then neither method can be used to obtain the outcome.
+     *
+     * <p> This method may only be invoked by the scope owner.
+     *
+     * @param task the value-returning task for the thread to execute
+     * @param <U> the result type
+     * @return the subtask
+     * @throws WrongThreadException if the current thread is not the scope owner
+     * @throws IllegalStateException if the owner has already {@linkplain #join() joined}
+     * or the scope is closed
+     * @throws StructureViolationException if the current scoped value bindings are not
+     * the same as when the scope was created
+     * @throws RejectedExecutionException if the thread factory rejected creating a
+     * thread to run the subtask
+     */
+    <U extends T> Subtask<U> fork(Callable<? extends U> task);
+
+    /**
+     * Fork a subtask by starting a new thread in this scope to execute a method that
+     * does not return a result.
+     *
+     * <p> This method works exactly the same as {@link #fork(Callable)} except that the
+     * parameter to this method is a {@link Runnable}, the new thread executes its
+     * {@link Runnable#run() run} method, and {@link Subtask#get() Subtask.get()} returns
+     * {@code null} if the subtask completes successfully.
+     *
+     * @param task the task for the thread to execute
+     * @param <U> the result type
+     * @return the subtask
+     * @throws WrongThreadException if the current thread is not the scope owner
+     * @throws IllegalStateException if the owner has already {@linkplain #join() joined}
+     * or the scope is closed
+     * @throws StructureViolationException if the current scoped value bindings are not
+     * the same as when the scope was created
+     * @throws RejectedExecutionException if the thread factory rejected creating a
+     * thread to run the subtask
+     * @since 24
+     */
+    <U extends T> Subtask<U> fork(Runnable task);
+
+    /**
+     * Returns the result, or throws, after waiting for all subtasks to complete or
+     * the scope to be <a href="#Cancallation">cancelled</a>.
+     *
+     * <p> This method waits for all subtasks started in this scope to complete or the
+     * scope to be cancelled. If a {@linkplain Config#withTimeout(Duration) timeout} is
+     * configured and the timeout expires before or while waiting, then the scope is
+     * cancelled and {@link TimeoutException TimeoutException} is thrown. Once finished
+     * waiting, the {@code Joiner}'s {@link Joiner#result() result()} method is invoked
+     * to get the result or throw an exception. If the {@code result()} method throws
+     * then this method throws {@code FailedException} with the exception as the cause.
+     *
+     * <p> This method may only be invoked by the scope owner, and only once.
+     *
+     * @return the result
+     * @throws WrongThreadException if the current thread is not the scope owner
+     * @throws IllegalStateException if already joined or this scope is closed
+     * @throws FailedException if the <i>outcome</i> is an exception, thrown with the
+     * exception from {@link Joiner#result() Joiner.result()} as the cause
+     * @throws TimeoutException if a timeout is set and the timeout expires before or
+     * while waiting
+     * @throws InterruptedException if interrupted while waiting
+     * @since 24
+     */
+    R join() throws InterruptedException;
+
+    /**
+     * {@return {@code true} if this scope is <a href="#Cancallation">cancelled</a> or in
+     * the process of being cancelled, otherwise {@code false}}
+     *
+     * <p> Cancelling the scope prevents new threads from starting in the scope and
+     * {@linkplain Thread#interrupt() interrupts} threads executing unfinished subtasks.
+     * It may take some time before the interrupted threads finish execution; this
+     * method may return {@code true} before all threads have been interrupted or before
+     * all threads have finished.
+     *
+     * @apiNote A task with a lengthy "forking phase" (the code that executes before
+     * it invokes {@link #join() join}) may use this method to avoid doing work in cases
+     * where scope is cancelled by the completion of a previously forked subtask or timeout.
+     *
+     * @since 24
+     */
+    boolean isCancelled();
+
+    /**
+     * Closes this scope.
+     *
+     * <p> This method first <a href="#Cancallation">cancels</a> the scope, if not
+     * already cancelled. This interrupts the threads executing unfinished subtasks. This
+     * method then waits for all threads to finish. If interrupted while waiting then it
+     * will continue to wait until the threads finish, before completing with the interrupt
+     * status set.
+     *
+     * <p> This method may only be invoked by the scope owner. If the scope
+     * is already closed then the scope owner invoking this method has no effect.
+     *
+     * <p> A {@code StructuredTaskScope} is intended to be used in a <em>structured
+     * manner</em>. If this method is called to close a scope before nested task
+     * scopes are closed then it closes the underlying construct of each nested scope
+     * (in the reverse order that they were created in), closes this scope, and then
+     * throws {@link StructureViolationException}.
+     * Similarly, if this method is called to close a scope while executing with
+     * {@linkplain ScopedValue scoped value} bindings, and the scope was created
+     * before the scoped values were bound, then {@code StructureViolationException} is
+     * thrown after closing the scope.
+     * If a thread terminates without first closing scopes that it owns then
+     * termination will cause the underlying construct of each of its open tasks scopes to
+     * be closed. Closing is performed in the reverse order that the scopes were
+     * created in. Thread termination may therefore be delayed when the scope owner
+     * has to wait for threads forked in these scopes to finish.
+     *
+     * @throws IllegalStateException thrown after closing the scope if the scope
+     * owner did not attempt to join after forking
+     * @throws WrongThreadException if the current thread is not the scope owner
+     * @throws StructureViolationException if a structure violation was detected
+     */
+    @Override
+    void close();
+}
diff a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScopeImpl.java b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScopeImpl.java
--- /dev/null
+++ b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScopeImpl.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package java.util.concurrent;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.function.Function;
+import jdk.internal.misc.InnocuousThread;
+import jdk.internal.misc.ThreadFlock;
+import jdk.internal.invoke.MhUtil;
+
+/**
+ * StructuredTaskScope implementation.
+ */
+final class StructuredTaskScopeImpl<T, R> implements StructuredTaskScope<T, R> {
+    private static final VarHandle CANCELLED =
+            MhUtil.findVarHandle(MethodHandles.lookup(), "cancelled", boolean.class);
+
+    private final Joiner<? super T, ? extends R> joiner;
+    private final ThreadFactory threadFactory;
+    private final ThreadFlock flock;
+
+    // state, only accessed by owner thread
+    private static final int ST_NEW            = 0;
+    private static final int ST_FORKED         = 1;   // subtasks forked, need to join
+    private static final int ST_JOIN_STARTED   = 2;   // join started, can no longer fork
+    private static final int ST_JOIN_COMPLETED = 3;   // join completed
+    private static final int ST_CLOSED         = 4;   // closed
+    private int state;
+
+    // timer task, only accessed by owner thread
+    private Future<?> timerTask;
+
+    // set or read by any thread
+    private volatile boolean cancelled;
+
+    // set by the timer thread, read by the owner thread
+    private volatile boolean timeoutExpired;
+
+    @SuppressWarnings("this-escape")
+    private StructuredTaskScopeImpl(Joiner<? super T, ? extends R> joiner,
+                                    ThreadFactory threadFactory,
+                                    String name) {
+        this.joiner = joiner;
+        this.threadFactory = threadFactory;
+        this.flock = ThreadFlock.open((name != null) ? name : Objects.toIdentityString(this));
+    }
+
+    /**
+     * Returns a new {@code StructuredTaskScope} to use the given {@code Joiner} object
+     * and with configuration that is the result of applying the given function to the
+     * default configuration.
+     */
+    static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner,
+                                                 Function<Config, Config> configFunction) {
+        Objects.requireNonNull(joiner);
+
+        var config = (ConfigImpl) configFunction.apply(ConfigImpl.defaultConfig());
+        var scope = new StructuredTaskScopeImpl<T, R>(joiner, config.threadFactory(), config.name());
+
+        // schedule timeout
+        Duration timeout = config.timeout();
+        if (timeout != null) {
+            boolean scheduled = false;
+            try {
+                scope.scheduleTimeout(timeout);
+                scheduled = true;
+            } finally {
+                if (!scheduled) {
+                    scope.close();  // pop if scheduling timeout failed
+                }
+            }
+        }
+
+        return scope;
+    }
+
+    /**
+     * Throws WrongThreadException if the current thread is not the owner thread.
+     */
+    private void ensureOwner() {
+        if (Thread.currentThread() != flock.owner()) {
+            throw new WrongThreadException("Current thread not owner");
+        }
+    }
+
+    /**
+     * Throws IllegalStateException if already joined or scope is closed.
+     */
+    private void ensureNotJoined() {
+        assert Thread.currentThread() == flock.owner();
+        if (state > ST_FORKED) {
+            throw new IllegalStateException("Already joined or scope is closed");
+        }
+    }
+
+    /**
+     * Throws IllegalStateException if invoked by the owner thread and the owner thread
+     * has not joined.
+     */
+    private void ensureJoinedIfOwner() {
+        if (Thread.currentThread() == flock.owner() && state <= ST_JOIN_STARTED) {
+            throw new IllegalStateException("join not called");
+        }
+    }
+
+    /**
+     * Interrupts all threads in this scope, except the current thread.
+     */
+    private void interruptAll() {
+        flock.threads()
+                .filter(t -> t != Thread.currentThread())
+                .forEach(t -> {
+                    try {
+                        t.interrupt();
+                    } catch (Throwable ignore) { }
+                });
+    }
+
+    /**
+     * Cancel the scope if not already cancelled.
+     */
+    private void cancel() {
+        if (!cancelled && CANCELLED.compareAndSet(this, false, true)) {
+            // prevent new threads from starting
+            flock.shutdown();
+
+            // interrupt all unfinished threads
+            interruptAll();
+
+            // wakeup join
+            flock.wakeup();
+        }
+    }
+
+    /**
+     * Schedules a task to cancel the scope on timeout.
+     */
+    private void scheduleTimeout(Duration timeout) {
+        assert Thread.currentThread() == flock.owner() && timerTask == null;
+        timerTask = TimerSupport.schedule(timeout, () -> {
+            if (!cancelled) {
+                timeoutExpired = true;
+                cancel();
+            }
+        });
+    }
+
+    /**
+     * Cancels the timer task if set.
+     */
+    private void cancelTimeout() {
+        assert Thread.currentThread() == flock.owner();
+        if (timerTask != null) {
+            timerTask.cancel(false);
+        }
+    }
+
+    /**
+     * Invoked by the thread for a subtask when the subtask completes before scope is cancelled.
+     */
+    private void onComplete(SubtaskImpl<? extends T> subtask) {
+        assert subtask.state() != Subtask.State.UNAVAILABLE;
+        if (joiner.onComplete(subtask)) {
+            cancel();
+        }
+    }
+
+    @Override
+    public <U extends T> Subtask<U> fork(Callable<? extends U> task) {
+        Objects.requireNonNull(task);
+        ensureOwner();
+        ensureNotJoined();
+
+        var subtask = new SubtaskImpl<U>(this, task);
+
+        // notify joiner, even if cancelled
+        if (joiner.onFork(subtask)) {
+            cancel();
+        }
+
+        if (!cancelled) {
+            // create thread to run task
+            Thread thread = threadFactory.newThread(subtask);
+            if (thread == null) {
+                throw new RejectedExecutionException("Rejected by thread factory");
+            }
+
+            // attempt to start the thread
+            try {
+                flock.start(thread);
+            } catch (IllegalStateException e) {
+                // shutdown by another thread, or underlying flock is shutdown due
+                // to unstructured use
+            }
+        }
+
+        // force owner to join
+        state = ST_FORKED;
+        return subtask;
+    }
+
+    @Override
+    public <U extends T> Subtask<U> fork(Runnable task) {
+        Objects.requireNonNull(task);
+        return fork(() -> { task.run(); return null; });
+    }
+
+    @Override
+    public R join() throws InterruptedException {
+        ensureOwner();
+        ensureNotJoined();
+
+        // join started
+        state = ST_JOIN_STARTED;
+
+        // wait for all subtasks, the scope to be cancelled, or interrupt
+        flock.awaitAll();
+
+        // throw if timeout expired
+        if (timeoutExpired) {
+            throw new TimeoutException();
+        }
+        cancelTimeout();
+
+        // all subtasks completed or cancelled
+        state = ST_JOIN_COMPLETED;
+
+        // invoke joiner to get result
+        try {
+            return joiner.result();
+        } catch (Throwable e) {
+            throw new FailedException(e);
+        }
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return cancelled;
+    }
+
+    @Override
+    public void close() {
+        ensureOwner();
+        int s = state;
+        if (s == ST_CLOSED) {
+            return;
+        }
+
+        // cancel the scope if join did not complete
+        if (s < ST_JOIN_COMPLETED) {
+            cancel();
+            cancelTimeout();
+        }
+
+        // wait for stragglers
+        try {
+            flock.close();
+        } finally {
+            state = ST_CLOSED;
+        }
+
+        // throw ISE if the owner didn't join after forking
+        if (s == ST_FORKED) {
+            throw new IllegalStateException("Owner did not join after forking");
+        }
+    }
+
+    @Override
+    public String toString() {
+        return flock.name();
+    }
+
+    /**
+     * Subtask implementation, runs the task specified to the fork method.
+     */
+    static final class SubtaskImpl<T> implements Subtask<T>, Runnable {
+        private static final AltResult RESULT_NULL = new AltResult(Subtask.State.SUCCESS);
+
+        private record AltResult(Subtask.State state, Throwable exception) {
+            AltResult(Subtask.State state) {
+                this(state, null);
+            }
+        }
+
+        private final StructuredTaskScopeImpl<? super T, ?> scope;
+        private final Callable<? extends T> task;
+        private volatile Object result;
+
+        SubtaskImpl(StructuredTaskScopeImpl<? super T, ?> scope, Callable<? extends T> task) {
+            this.scope = scope;
+            this.task = task;
+        }
+
+        @Override
+        public void run() {
+            T result = null;
+            Throwable ex = null;
+            try {
+                result = task.call();
+            } catch (Throwable e) {
+                ex = e;
+            }
+
+            // nothing to do if scope is cancelled
+            if (scope.isCancelled())
+                return;
+
+            // set result/exception and invoke onComplete
+            if (ex == null) {
+                this.result = (result != null) ? result : RESULT_NULL;
+            } else {
+                this.result = new AltResult(State.FAILED, ex);
+            }
+            scope.onComplete(this);
+        }
+
+        @Override
+        public Subtask.State state() {
+            Object result = this.result;
+            if (result == null) {
+                return State.UNAVAILABLE;
+            } else if (result instanceof AltResult alt) {
+                // null or failed
+                return alt.state();
+            } else {
+                return State.SUCCESS;
+            }
+        }
+
+        @Override
+        public T get() {
+            scope.ensureJoinedIfOwner();
+            Object result = this.result;
+            if (result instanceof AltResult) {
+                if (result == RESULT_NULL) return null;
+            } else if (result != null) {
+                @SuppressWarnings("unchecked")
+                T r = (T) result;
+                return r;
+            }
+            throw new IllegalStateException(
+                    "Result is unavailable or subtask did not complete successfully");
+        }
+
+        @Override
+        public Throwable exception() {
+            scope.ensureJoinedIfOwner();
+            Object result = this.result;
+            if (result instanceof AltResult alt && alt.state() == State.FAILED) {
+                return alt.exception();
+            }
+            throw new IllegalStateException(
+                    "Exception is unavailable or subtask did not complete with exception");
+        }
+
+        @Override
+        public String toString() {
+            String stateAsString = switch (state()) {
+                case UNAVAILABLE -> "[Unavailable]";
+                case SUCCESS     -> "[Completed successfully]";
+                case FAILED      -> {
+                    Throwable ex = ((AltResult) result).exception();
+                    yield "[Failed: " + ex + "]";
+                }
+            };
+            return Objects.toIdentityString(this) + stateAsString;
+        }
+    }
+
+    /**
+     * Config implementation.
+     */
+    record ConfigImpl(ThreadFactory threadFactory,
+                      String name,
+                      Duration timeout) implements Config {
+        static Config defaultConfig() {
+            return new ConfigImpl(Thread.ofVirtual().factory(), null, null);
+        }
+
+        @Override
+        public Config withThreadFactory(ThreadFactory threadFactory) {
+            return new ConfigImpl(Objects.requireNonNull(threadFactory), name, timeout);
+        }
+
+        @Override
+        public Config withName(String name) {
+            return new ConfigImpl(threadFactory, Objects.requireNonNull(name), timeout);
+        }
+
+        @Override
+        public Config withTimeout(Duration timeout) {
+            return new ConfigImpl(threadFactory, name, Objects.requireNonNull(timeout));
+        }
+    }
+
+    /**
+     * Used to schedule a task to cancel the scope when a timeout expires.
+     */
+    private static class TimerSupport {
+        private static final ScheduledExecutorService DELAYED_TASK_SCHEDULER;
+        static {
+            ScheduledThreadPoolExecutor stpe = (ScheduledThreadPoolExecutor)
+                    Executors.newScheduledThreadPool(1, task -> {
+                        Thread t = InnocuousThread.newThread("StructuredTaskScope-Timer", task);
+                        t.setDaemon(true);
+                        return t;
+                    });
+            stpe.setRemoveOnCancelPolicy(true);
+            DELAYED_TASK_SCHEDULER = stpe;
+        }
+
+        static Future<?> schedule(Duration timeout, Runnable task) {
+            long nanos = TimeUnit.NANOSECONDS.convert(timeout);
+            return DELAYED_TASK_SCHEDULER.schedule(task, nanos, TimeUnit.NANOSECONDS);
+        }
+    }
+}
diff a/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java b/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java
--- a/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java
+++ b/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java
@@ -270,19 +270,12 @@
 
     /**
      * Shutdown this flock so that no new threads can be started, existing threads
      * in the flock will continue to run. This method is a no-op if the flock is
      * already shutdown or closed.
-     *
-     * <p> This method may only be invoked by the flock owner or threads {@linkplain
-     * #containsThread(Thread) contained} in the flock.
-     *
-     * @throws WrongThreadException if the current thread is not the owner or a thread
-     * contained in the flock
      */
     public void shutdown() {
-        ensureOwnerOrContainsThread();
         if (!shutdown) {
             shutdown = true;
         }
     }
 
@@ -368,16 +361,12 @@
      * {@linkplain #owner() owner} to return immediately.
      *
      * <p> If the owner is blocked in {@code awaitAll} then it will return immediately.
      * If the owner is not blocked in {@code awaitAll} then its next call to wait
      * will return immediately. The method does nothing when the flock is closed.
-     *
-     * @throws WrongThreadException if the current thread is not the owner or a thread
-     * contained in the flock
      */
     public void wakeup() {
-        ensureOwnerOrContainsThread();
         if (!getAndSetPermit(true) && Thread.currentThread() != owner()) {
             LockSupport.unpark(owner());
         }
     }
 
diff a/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java b/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java
--- a/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java
+++ b/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java
@@ -158,43 +158,46 @@
                         threadMBean);
             }
         });
 
         /**
-         * VirtualThreadSchedulerMXBean.
+         * VirtualThreadSchedulerMXBean registered when using JDK's virtual thread scheduler.
          */
-        initMBeanList.add(new PlatformComponent<VirtualThreadSchedulerMXBean>() {
-            private final Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces =
-                    Set.of(VirtualThreadSchedulerMXBean.class);
-            private final Set<String> mbeanInterfaceNames =
-                    Set.of(VirtualThreadSchedulerMXBean.class.getName());
-            private VirtualThreadSchedulerMXBean impl;
-
-            @Override
-            public Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces() {
-                return mbeanInterfaces;
-            }
+        String value = System.getProperty("jdk.virtualThreadScheduler.implClass");
+        if (value == null) {
+            initMBeanList.add(new PlatformComponent<VirtualThreadSchedulerMXBean>() {
+                private final Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces =
+                        Set.of(VirtualThreadSchedulerMXBean.class);
+                private final Set<String> mbeanInterfaceNames =
+                        Set.of(VirtualThreadSchedulerMXBean.class.getName());
+                private VirtualThreadSchedulerMXBean impl;
+
+                @Override
+                public Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces() {
+                    return mbeanInterfaces;
+                }
 
-            @Override
-            public Set<String> mbeanInterfaceNames() {
-                return mbeanInterfaceNames;
-            }
+                @Override
+                public Set<String> mbeanInterfaceNames() {
+                    return mbeanInterfaceNames;
+                }
 
-            @Override
-            public String getObjectNamePattern() {
-                return "jdk.management:type=VirtualThreadScheduler";
-            }
+                @Override
+                public String getObjectNamePattern() {
+                    return "jdk.management:type=VirtualThreadScheduler";
+                }
 
-            @Override
-            public Map<String, VirtualThreadSchedulerMXBean> nameToMBeanMap() {
-                VirtualThreadSchedulerMXBean impl = this.impl;
-                if (impl == null) {
-                    this.impl = impl = VirtualThreadSchedulerImpls.create();
+                @Override
+                public Map<String, VirtualThreadSchedulerMXBean> nameToMBeanMap() {
+                    VirtualThreadSchedulerMXBean impl = this.impl;
+                    if (impl == null) {
+                        this.impl = impl = VirtualThreadSchedulerImpls.create();
+                    }
+                    return Map.of("jdk.management:type=VirtualThreadScheduler", impl);
                 }
-                return Map.of("jdk.management:type=VirtualThreadScheduler", impl);
-            }
-        });
+            });
+        }
 
         /**
          * OperatingSystemMXBean
          */
         initMBeanList.add(new PlatformComponent<OperatingSystemMXBean>() {
diff a/test/hotspot/jtreg/ProblemList-Virtual.txt b/test/hotspot/jtreg/ProblemList-Virtual.txt
--- a/test/hotspot/jtreg/ProblemList-Virtual.txt
+++ b/test/hotspot/jtreg/ProblemList-Virtual.txt
@@ -90,11 +90,13 @@
 ###
 # Fails because resume of a virtual thread is not enough to allow the virtual thread
 # to make progress when all other threads are currently suspended.
 vmTestbase/nsk/jdi/ThreadReference/isSuspended/issuspended002/TestDescription.java 8338713 generic-all
 
-###
+# Unknown reasons, should be investigated
+vmTestbase/gc/gctests/MemoryEaterMT/MemoryEaterMT.java 0000000 generic-all
+
 # Fails on Windows because of additional memory allocation.
 
 gc/g1/TestMixedGCLiveThreshold.java#25percent 8334759 windows-x64
 
 ##########
@@ -108,5 +110,22 @@
 runtime/ErrorHandling/MachCodeFramesInErrorFile.java 0000000 generic-all
 runtime/logging/LoaderConstraintsTest.java JDK-8346442 generic-all
 runtime/Thread/AsyncExceptionOnMonitorEnter.java 0000000 generic-all
 runtime/Thread/StopAtExit.java 0000000 generic-all
 runtime/handshake/HandshakeWalkStackTest.java 0000000 generic-all
+
+# java.lang.ClassNotFoundException: jdk.test.lib.process.ProcessTools
+runtime/cds/appcds/jigsaw/classpathtests/EmptyClassInBootClassPath.java 0000000 generic-all
+
+#  java.lang.ClassNotFoundException: / in ProcessTools.main
+runtime/cds/appcds/jigsaw/modulepath/OptimizeModuleHandlingTest.java 0000000 generic-all
+
+# too fragile when main thread is a virtual thread
+runtime/Thread/ThreadCountLimit.java#id0 0000000 generic-all
+
+# Invalid code heap sizes
+compiler/arguments/TestC1Globals.java#id0 0000000 generic-all
+compiler/arguments/TestC1Globals.java#id1 0000000 generic-all
+compiler/arguments/TestC1Globals.java#id2 0000000 generic-all
+
+# OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
+compiler/codecache/CodeCacheFullCountTest.java 8332926 generic-all
diff a/test/hotspot/jtreg/ProblemList-Xcomp.txt b/test/hotspot/jtreg/ProblemList-Xcomp.txt
--- a/test/hotspot/jtreg/ProblemList-Xcomp.txt
+++ b/test/hotspot/jtreg/ProblemList-Xcomp.txt
@@ -53,5 +53,19 @@
 vmTestbase/nsk/stress/thread/thread006.java 8321476 linux-all
 
 compiler/cha/TypeProfileFinalMethod.java 8341039 generic-all
 
 gc/arguments/TestNewSizeFlags.java 8299116 macosx-aarch64
+
+############################################################################
+
+# Loom, fibers branch
+
+gc/g1/plab/TestPLABPromotion.java                                       8278126 generic-all
+gc/g1/plab/TestPLABResize.java                                          8278126 generic-all
+
+gc/stringdedup/TestStringDeduplicationInterned.java#G1                  8278127 generic-all
+gc/stringdedup/TestStringDeduplicationInterned.java#Parallel            8278127 generic-all
+gc/stringdedup/TestStringDeduplicationInterned.java#Serial              8278127 generic-all
+gc/stringdedup/TestStringDeduplicationInterned.java#Z                   8278127 generic-all
+
+gc/z/TestGarbageCollectorMXBean.java                                    8278129 generic-all
diff a/test/hotspot/jtreg/ProblemList-vthread-Xcomp.txt b/test/hotspot/jtreg/ProblemList-vthread-Xcomp.txt
--- /dev/null
+++ b/test/hotspot/jtreg/ProblemList-vthread-Xcomp.txt
@@ -0,0 +1,34 @@
+#
+# Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+
+#############################################################################
+#
+# List of quarantined tests for testing in Xcomp mode with wrapper.
+#
+#############################################################################
+
+####
+## Unexpected results
+compiler/uncommontrap/TestUnstableIfTrap.java
+gc/g1/plab/TestPLABPromotion.java
+gc/whitebox/TestWBGC.java
diff a/test/hotspot/jtreg/ProblemList.txt b/test/hotspot/jtreg/ProblemList.txt
--- a/test/hotspot/jtreg/ProblemList.txt
+++ b/test/hotspot/jtreg/ProblemList.txt
@@ -142,10 +142,18 @@
 serviceability/sa/ClhsdbPmap.java#core            8267433,8318754 macosx-x64,macosx-aarch64
 serviceability/sa/ClhsdbPstack.java#core          8267433,8318754 macosx-x64,macosx-aarch64
 serviceability/sa/TestJmapCore.java               8267433,8318754 macosx-x64,macosx-aarch64
 serviceability/sa/TestJmapCoreMetaspace.java      8267433,8318754 macosx-x64,macosx-aarch64
 
+serviceability/sa/ClhsdbFindPC.java#xcomp-core 8284045 macosx-aarch64
+serviceability/sa/ClhsdbCDSCore.java 8284045 macosx-aarch64
+serviceability/sa/TestJmapCore.java 8284045 macosx-aarch64
+
+# "waiting on ..." missing from stdout/stderr
+serviceability/sa/TestClhsdbJstackLock.java 0000000 generic-all
+serviceability/sa/TestJhsdbJstackLock.java 0000000 generic-all
+
 serviceability/attach/ConcAttachTest.java 8290043 linux-all
 
 serviceability/jvmti/stress/StackTrace/NotSuspended/GetStackTraceNotSuspendedStressTest.java 8315980 linux-all,windows-x64
 
 #############################################################################
diff a/test/hotspot/jtreg/TEST.groups b/test/hotspot/jtreg/TEST.groups
--- a/test/hotspot/jtreg/TEST.groups
+++ b/test/hotspot/jtreg/TEST.groups
@@ -822,5 +822,10 @@
 # JDB tests
 vmTestbase_nsk_jdb = \
   vmTestbase/nsk/jdb
 
 # vmTestbase_*_quick groups are defined in TEST.quick-groups
+
+hotspot_remaining = \
+ :hotspot_all_no_apps \
+  -vmTestbase \
+  -:tier1
diff a/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java b/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java
--- a/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java
+++ b/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -23,10 +23,11 @@
 
 /*
  * @test
  * @bug 8284161 8287008
  * @summary Basic test for jcmd Thread.dump_to_file
+ * @modules jdk.jcmd
  * @library /test/lib
  * @run junit/othervm ThreadDumpToFileTest
  */
 
 import java.io.IOException;
diff a/test/jaxp/ProblemList-Virtual.txt b/test/jaxp/ProblemList-Virtual.txt
--- /dev/null
+++ b/test/jaxp/ProblemList-Virtual.txt
@@ -0,0 +1,4 @@
+####
+## Tests using custom path and con't be executed with virtual test thread factory
+
+javax/xml/jaxp/module/ServiceProviderTest/BasicModularXMLParserTest.java 000000 generic-all
diff a/test/jdk/ProblemList-Virtual.txt b/test/jdk/ProblemList-Virtual.txt
--- a/test/jdk/ProblemList-Virtual.txt
+++ b/test/jdk/ProblemList-Virtual.txt
@@ -42,30 +42,78 @@
 
 javax/management/remote/mandatory/connection/DeadLockTest.java 8309069 windows-x64
 
 javax/management/remote/mandatory/connection/ConnectionTest.java 8308352 windows-x64
 
+## DatagramSocket blocks ops are interruptible when invoked on virtual threads
+java/nio/channels/DatagramChannel/InterruptibleOrNot.java 0000000 generic-all
+java/net/DatagramSocket/InterruptibleDatagramSocket.java 0000000 generic-all
+
+# Underlying implementation is non-blocking
+java/nio/channels/SocketChannel/ShortWrite.java 0000000 generic-all
+
+jdk/jfr/event/compiler/TestCompilerCompile.java 8325392 generic-all
+
 ##########
 ## Tests incompatible with virtual test thread factory.
 ## There is no goal to run all test with virtual test thread factory.
 ## So any test might be added as incompatible, the bug id is not required.
 
 # Incorrect stack/threadgroup/exception expectations for main thread
+com/sun/net/httpserver/bugs/B6431193.java 0000000 generic-all
 java/lang/StackWalker/DumpStackTest.java 0000000 generic-all
 java/lang/StackWalker/StackWalkTest.java 0000000 generic-all
 java/lang/StackWalker/CallerFromMain.java 0000000 generic-all
 java/lang/Thread/MainThreadTest.java 0000000 generic-all
 java/lang/Thread/UncaughtExceptionsTest.java 0000000 generic-all
-java/lang/Thread/virtual/GetStackTraceWhenRunnable.java 0000000 generic-all
 java/lang/invoke/condy/CondyNestedResolutionTest.java 0000000 generic-all
 java/lang/ref/OOMEInReferenceHandler.java 0000000 generic-all
 java/util/concurrent/locks/Lock/OOMEInAQS.java 0000000 generic-all
+java/util/concurrent/locks/StampedLock/OOMEInStampedLock.java 0000000 generic-all
 jdk/internal/vm/Continuation/Scoped.java 0000000 generic-all
+jdk/jfr/event/runtime/TestThreadDumpEvent.java 0000000 generic-all
+jdk/jfr/event/runtime/TestThreadStartEvent.java 0000000 generic-all
+jdk/jfr/jvm/TestGetAllEventClasses.java 0000000 generic-all
 
 # The problems with permissions
-jdk/jfr/startupargs/TestDumpOnExit.java 0000000 generic-all
+java/io/File/GetXSpace.java 0000000 generic-all
+java/io/FilePermission/MergeName.java 0000000 generic-all
 java/lang/SecurityManager/modules/CustomSecurityManagerTest.java 0000000 generic-all
+java/net/httpclient/security/Security.java 0000000 generic-all
+java/security/AccessController/LimitedDoPrivileged.java	0000000 generic-all
 java/util/PluggableLocale/PermissionTest.java 0000000 generic-all
 java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all
 java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all
 javax/management/ImplementationVersion/ImplVersionTest.java 0000000 generic-all
 javax/management/remote/mandatory/version/ImplVersionTest.java 0000000 generic-all
+
+
+jdk/jfr/api/consumer/security/TestStreamingRemote.java 0000000 generic-all
+jdk/jfr/api/consumer/TestRecordedEventGetThread.java 0000000 generic-all
+jdk/jfr/api/consumer/TestRecordedObject.java 0000000 generic-all
+jdk/jfr/startupargs/TestDumpOnExit.java 0000000 generic-all
+
+# AccessControlException: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
+sun/security/util/Resources/early/EarlyResources.java 0000000 generic-all
+
+# ProcessTools fails with InaccessibleObjectException
+tools/jimage/JImageToolTest.java 0000000 generic-all
+
+com/sun/management/DiagnosticCommandMBean/DcmdMBeanPermissionsTest.java 0000000 generic-all
+
+# Not supported on virtual threads
+com/sun/management/ThreadMXBean/ThreadAllocatedMemory.java#G1 0000000 generic-all
+com/sun/management/ThreadMXBean/ThreadAllocatedMemory.java#Serial 0000000 generic-all
+java/lang/management/ManagementFactory/ProxyTypeMapping.java 0000000 generic-all
+java/lang/management/ManagementFactory/ValidateOpenTypes.java 0000000 generic-all
+java/lang/management/ThreadMXBean/Locks.java 0000000 generic-all
+java/lang/management/ThreadMXBean/LockedMonitorInNative.java 0000000 generic-all
+java/lang/management/ThreadMXBean/ThreadCpuTime.java 0000000 generic-all
+java/lang/management/ThreadMXBean/ThreadUserTime.java 0000000 generic-all
+
+# Too sensitive to thread counts
+java/lang/management/ThreadMXBean/ResetPeakThreadCount.java 0000000 generic-all
+
+# need to be reexamined
+sun/management/jmxremote/startstop/JMXStatusTest.java 0000000 generic-all
+sun/management/jmxremote/startstop/JMXStatusPerfCountersTest.java 0000000 generic-all
+sun/management/jmxremote/startstop/JMXStartStopTest.java 0000000 generic-all
diff a/test/jdk/ProblemList-vthread-Xcomp.txt b/test/jdk/ProblemList-vthread-Xcomp.txt
--- /dev/null
+++ b/test/jdk/ProblemList-vthread-Xcomp.txt
@@ -0,0 +1,29 @@
+#
+# Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+
+#############################################################################
+#
+# List of quarantined tests for testing in Xcomp mode with wrapper.
+#
+#############################################################################
+
diff a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt
--- a/test/jdk/ProblemList.txt
+++ b/test/jdk/ProblemList.txt
@@ -821,5 +821,10 @@
 java/awt/List/HandlingKeyEventIfMousePressedTest.java 6848358 macosx-all,windows-all
 java/awt/Checkbox/CheckboxBoxSizeTest.java 8340870 windows-all
 java/awt/Checkbox/CheckboxIndicatorSizeTest.java 8340870 windows-all
 java/awt/Checkbox/CheckboxNullLabelTest.java 8340870 windows-all
 java/awt/dnd/WinMoveFileToShellTest.java 8341665 windows-all
+
+############################################################################
+
+# TypeElement to indicate class or interface included in id
+tools/sincechecker/modules/java.base/JavaBaseCheckSince.java 8343598 generic-all
diff a/test/jdk/java/lang/ScopedValue/StressStackOverflow.java b/test/jdk/java/lang/ScopedValue/StressStackOverflow.java
--- a/test/jdk/java/lang/ScopedValue/StressStackOverflow.java
+++ b/test/jdk/java/lang/ScopedValue/StressStackOverflow.java
@@ -50,10 +50,11 @@
 import java.lang.ScopedValue.CallableOp;
 import java.time.Duration;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.StructureViolationException;
 import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.util.function.Supplier;
 
 public class StressStackOverflow {
     public static final ScopedValue<Integer> el = ScopedValue.newInstance();
 
@@ -167,11 +168,11 @@
 
     // Run op in a new thread. Platform or virtual threads are chosen at random.
     void runInNewThread(Runnable op) {
         var threadFactory
                 = (ThreadLocalRandom.current().nextBoolean() ? Thread.ofPlatform() : Thread.ofVirtual()).factory();
-        try (var scope = new StructuredTaskScope<>("", threadFactory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withThreadFactory(threadFactory))) {
             var handle = scope.fork(() -> {
                 op.run();
                 return null;
             });
             scope.join();
@@ -184,11 +185,11 @@
     }
 
     public void run() {
         try {
             ScopedValue.where(inheritedValue, 42).where(el, 0).run(() -> {
-                try (var scope = new StructuredTaskScope<>()) {
+                try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
                     try {
                         if (ThreadLocalRandom.current().nextBoolean()) {
                             // Repeatedly test Scoped Values set by ScopedValue::call(), get(), and run()
                             final var deepRecursion
                                 = new DeepRecursion(DeepRecursion.Behaviour.choose(ThreadLocalRandom.current()));
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java
--- /dev/null
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Stress test of StructuredTaskScope cancellation with running and starting threads
+ * @enablePreview
+ * @run junit StressCancellation
+ */
+
+import java.time.Duration;
+import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.Joiner;
+import java.util.concurrent.StructuredTaskScope.Subtask;
+import java.util.concurrent.ThreadFactory;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import static org.junit.jupiter.api.Assertions.*;
+
+class StressCancellation {
+
+    static Stream<Arguments> testCases() {
+        Stream<ThreadFactory> factories = Stream.of(
+                Thread.ofPlatform().factory(),
+                Thread.ofVirtual().factory()
+        );
+        // 0..15 forks before shutdown, 0..15 forks after shutdown
+        return factories.flatMap(f -> IntStream.range(0, 256)
+                .mapToObj(x -> Arguments.of(f, x & 0x0F, (x & 0xF0) >> 4)));
+    }
+
+    /**
+     * Test StructuredTaskScope cancellation with running threads and concurrently with
+     * threads that are starting. The cancellation should interrupt all running threads,
+     * join should wakeup, and close would complete quickly.
+     *
+     * @param factory the ThreadFactory to use
+     * @param beforeCancel the number of subtasks to fork before cancel
+     * @param afterCancel the number of subtasks to fork after cancel
+     */
+    @ParameterizedTest
+    @MethodSource("testCases")
+    void test(ThreadFactory factory, int beforeCancel, int afterCancel) throws Exception {
+        var joiner = new Joiner<Boolean, Void>() {
+            @Override
+            public boolean onComplete(Subtask<? extends Boolean> subtask) {
+                boolean cancel = subtask.get();
+                return cancel;
+            }
+            @Override
+            public Void result() {
+                return null;
+            }
+        };
+
+        try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) {
+            // fork subtasks
+            for (int i = 0; i < beforeCancel; i++) {
+                scope.fork(() -> {
+                    Thread.sleep(Duration.ofDays(1));
+                    return false;
+                });
+            }
+
+            // fork subtask to cancel
+            scope.fork(() -> true);
+
+            // fork after forking subtask to cancel
+            for (int i = 0; i < afterCancel; i++) {
+                scope.fork(() -> {
+                    Thread.sleep(Duration.ofDays(1));
+                    return false;
+                });
+            }
+
+            scope.join();
+        }
+    }
+}
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StressShutdown.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StressShutdown.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/StressShutdown.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/*
- * @test
- * @bug 8311867
- * @summary Stress test of StructuredTaskScope.shutdown with running and starting threads
- * @enablePreview
- * @run junit StressShutdown
- */
-
-import java.time.Duration;
-import java.util.concurrent.Callable;
-import java.util.concurrent.StructuredTaskScope;
-import java.util.concurrent.ThreadFactory;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import static org.junit.jupiter.api.Assertions.*;
-
-class StressShutdown {
-
-    static final Callable<Void> SLEEP_FOR_A_DAY = () -> {
-        Thread.sleep(Duration.ofDays(1));
-        return null;
-    };
-
-    static Stream<Arguments> testCases() {
-        Stream<ThreadFactory> factories = Stream.of(
-                Thread.ofPlatform().factory(),
-                Thread.ofVirtual().factory()
-        );
-        // 0..15 forks before shutdown, 0..15 forks after shutdown
-        return factories.flatMap(f -> IntStream.range(0, 256)
-                .mapToObj(x -> Arguments.of(f, x & 0x0F, (x & 0xF0) >> 4)));
-    }
-
-    /**
-     * Test StructuredTaskScope.shutdown with running threads and concurrently with
-     * threads that are starting. The shutdown should interrupt all threads so that
-     * join wakes up.
-     *
-     * @param factory the ThreadFactory to use
-     * @param beforeShutdown the number of subtasks to fork before shutdown
-     * @param afterShutdown the number of subtasks to fork after shutdown
-     */
-    @ParameterizedTest
-    @MethodSource("testCases")
-    void testShutdown(ThreadFactory factory, int beforeShutdown, int afterShutdown)
-        throws InterruptedException
-    {
-        try (var scope = new StructuredTaskScope<>(null, factory)) {
-            // fork subtasks
-            for (int i = 0; i < beforeShutdown; i++) {
-                scope.fork(SLEEP_FOR_A_DAY);
-            }
-
-            // fork subtask to shutdown
-            scope.fork(() -> {
-                scope.shutdown();
-                return null;
-            });
-
-            // fork after forking subtask to shutdown
-            for (int i = 0; i < afterShutdown; i++) {
-                scope.fork(SLEEP_FOR_A_DAY);
-            }
-
-            scope.join();
-        }
-    }
-}
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -34,36 +34,37 @@
  * @enablePreview
  * @run junit/othervm -DthreadFactory=virtual StructuredTaskScopeTest
  */
 
 import java.time.Duration;
-import java.io.IOException;
-import java.time.Instant;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
+import java.util.NoSuchElementException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.concurrent.LinkedTransferQueue;
 import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeoutException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.TimeoutException;
+import java.util.concurrent.StructuredTaskScope.Config;
+import java.util.concurrent.StructuredTaskScope.FailedException;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.util.concurrent.StructuredTaskScope.Subtask;
-import java.util.concurrent.StructuredTaskScope.ShutdownOnSuccess;
-import java.util.concurrent.StructuredTaskScope.ShutdownOnFailure;
 import java.util.concurrent.StructureViolationException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Supplier;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 import static java.lang.Thread.State.*;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.BeforeAll;
@@ -99,36 +100,23 @@
     private static Stream<ThreadFactory> factories() {
         return threadFactories.stream();
     }
 
     /**
-     * Test that fork creates a new thread for each task.
+     * Test that fork creates virtual threads when no ThreadFactory is configured.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testForkCreatesThread(ThreadFactory factory) throws Exception {
-        Set<Long> tids = ConcurrentHashMap.newKeySet();
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            for (int i = 0; i < 100; i++) {
+    @Test
+    void testForkCreatesVirtualThread() throws Exception {
+        Set<Thread> threads = ConcurrentHashMap.newKeySet();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            for (int i = 0; i < 50; i++) {
+                // runnable
                 scope.fork(() -> {
-                    tids.add(Thread.currentThread().threadId());
-                    return null;
+                    threads.add(Thread.currentThread());
                 });
-            }
-            scope.join();
-        }
-        assertEquals(100, tids.size());
-    }
 
-    /**
-     * Test that fork creates a new virtual thread for each task.
-     */
-    @Test
-    void testForkCreateVirtualThread() throws Exception {
-        Set<Thread> threads = ConcurrentHashMap.newKeySet();
-        try (var scope = new StructuredTaskScope<Object>()) {
-            for (int i = 0; i < 100; i++) {
+                // callable
                 scope.fork(() -> {
                     threads.add(Thread.currentThread());
                     return null;
                 });
             }
@@ -137,745 +125,580 @@
         assertEquals(100, threads.size());
         threads.forEach(t -> assertTrue(t.isVirtual()));
     }
 
     /**
-     * Test that fork creates a new thread with the given thread factory.
+     * Test that fork create threads with the configured ThreadFactory.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testForkUsesFactory(ThreadFactory factory) throws Exception {
-        var count = new AtomicInteger();
-        ThreadFactory countingFactory = task -> {
-            count.incrementAndGet();
-            return factory.newThread(task);
-        };
-        try (var scope = new StructuredTaskScope<Object>(null, countingFactory)) {
-            for (int i = 0; i < 100; i++) {
-                scope.fork(() -> null);
+    void testForkUsesThreadFactory(ThreadFactory factory) throws Exception {
+        // TheadFactory that keeps reference to all threads it creates
+        class RecordingThreadFactory implements ThreadFactory {
+            final ThreadFactory delegate;
+            final Set<Thread> threads = ConcurrentHashMap.newKeySet();
+            RecordingThreadFactory(ThreadFactory delegate) {
+                this.delegate = delegate;
+            }
+            @Override
+            public Thread newThread(Runnable task) {
+                Thread thread = delegate.newThread(task);
+                threads.add(thread);
+                return thread;
+            }
+            Set<Thread> threads() {
+                return threads;
+            }
+        }
+        var recordingThreadFactory = new RecordingThreadFactory(factory);
+        Set<Thread> threads = ConcurrentHashMap.newKeySet();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(recordingThreadFactory))) {
+
+            for (int i = 0; i < 50; i++) {
+                // runnable
+                scope.fork(() -> {
+                    threads.add(Thread.currentThread());
+                });
+
+                // callable
+                scope.fork(() -> {
+                    threads.add(Thread.currentThread());
+                    return null;
+                });
             }
             scope.join();
         }
-        assertEquals(100, count.get());
+        assertEquals(100, threads.size());
+        assertEquals(recordingThreadFactory.threads(), threads);
     }
 
     /**
-     * Test fork is confined to threads in the scope "tree".
+     * Test fork method is owner confined.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testForkConfined(ThreadFactory factory) throws Exception {
-        try (var scope1 = new StructuredTaskScope<Boolean>();
-             var scope2 = new StructuredTaskScope<Boolean>()) {
-
-            // thread in scope1 cannot fork thread in scope2
-            Subtask<Boolean> subtask1 = scope1.fork(() -> {
-                assertThrows(WrongThreadException.class, () -> {
-                    scope2.fork(() -> null);
-                });
-                return true;
-            });
-
-            // thread in scope2 can fork thread in scope1
-            Subtask<Boolean> subtask2 = scope2.fork(() -> {
-                scope1.fork(() -> null);
-                return true;
-            });
-
-            scope2.join();
-            scope1.join();
-
-            assertTrue(subtask1.get());
-            assertTrue(subtask2.get());
+        try (var scope = StructuredTaskScope.open(Joiner.<Boolean>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             // random thread cannot fork
             try (var pool = Executors.newSingleThreadExecutor()) {
                 Future<Void> future = pool.submit(() -> {
                     assertThrows(WrongThreadException.class, () -> {
-                        scope1.fork(() -> null);
-                    });
-                    assertThrows(WrongThreadException.class, () -> {
-                        scope2.fork(() -> null);
+                        scope.fork(() -> null);
                     });
                     return null;
                 });
                 future.get();
             }
+
+            // subtask cannot fork
+            Subtask<Boolean> subtask = scope.fork(() -> {
+                assertThrows(WrongThreadException.class, () -> {
+                    scope.fork(() -> null);
+                });
+                return true;
+            });
+            scope.join();
+            assertTrue(subtask.get());
         }
     }
 
     /**
-     * Test fork after join completes.
+     * Test fork after join, no subtasks forked before join.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testForkAfterJoin(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            // round 1
-            var subtask1 = scope.fork(() -> "foo");
-            assertThrows(IllegalStateException.class, subtask1::get);
-            scope.join();
-            assertEquals("foo", subtask1.get());
-
-            // round 2
-            var subtask2 = scope.fork(() -> "bar");
-            assertEquals("foo", subtask1.get());
-            assertThrows(IllegalStateException.class, subtask2::get);
+    void testForkAfterJoin1(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.join();
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
+            assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar"));
+        }
+    }
 
-            // round 3
-            var subtask3 = scope.fork(() -> "baz");
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
-            assertThrows(IllegalStateException.class, subtask3::get);
+    /**
+     * Test fork after join, subtasks forked before join.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testForkAfterJoin2(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> "foo");
             scope.join();
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
-            assertEquals("baz", subtask3.get());
+            assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar"));
         }
     }
 
     /**
      * Test fork after join throws.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testForkAfterJoinThrows(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             var latch = new CountDownLatch(1);
             var subtask1 = scope.fork(() -> {
                 latch.await();
                 return "foo";
             });
 
             // join throws
             Thread.currentThread().interrupt();
             assertThrows(InterruptedException.class, scope::join);
 
-            // allow subtask1 to finish
-            latch.countDown();
-
-            // continue to fork
-            var subtask2 = scope.fork(() -> "bar");
-            assertThrows(IllegalStateException.class, subtask1::get);
-            assertThrows(IllegalStateException.class, subtask2::get);
-            scope.join();
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
+            // fork should throw
+            assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar"));
         }
     }
 
     /**
-     * Test fork after scope is shutdown.
+     * Test fork after task scope is cancelled. This test uses a custom Joiner to
+     * cancel execution.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testForkAfterShutdown(ThreadFactory factory) throws Exception {
-        var executed = new AtomicBoolean();
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            scope.shutdown();
-            Subtask<String> subtask = scope.fork(() -> {
-                executed.set(true);
-                return null;
-            });
+    void testForkAfterCancel2(ThreadFactory factory) throws Exception {
+        var countingThreadFactory = new CountingThreadFactory(factory);
+        var testJoiner = new CancelAfterOneJoiner<String>();
+
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(countingThreadFactory))) {
+
+            // fork subtask, the scope should be cancelled when the subtask completes
+            var subtask1 = scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
+
+            assertEquals(1, countingThreadFactory.threadCount());
+            assertEquals(1, testJoiner.onForkCount());
+            assertEquals(1, testJoiner.onCompleteCount());
+
+            // fork second subtask, it should not run
+            var subtask2 = scope.fork(() -> "bar");
+
+            // onFork should be invoked, newThread and onComplete should not be invoked
+            assertEquals(1, countingThreadFactory.threadCount());
+            assertEquals(2, testJoiner.onForkCount());
+            assertEquals(1, testJoiner.onCompleteCount());
+
             scope.join();
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalStateException.class, subtask::get);
-            assertThrows(IllegalStateException.class, subtask::exception);
+
+            assertEquals(1, countingThreadFactory.threadCount());
+            assertEquals(2, testJoiner.onForkCount());
+            assertEquals(1, testJoiner.onCompleteCount());
+            assertEquals("foo", subtask1.get());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
         }
-        assertFalse(executed.get());
     }
 
     /**
-     * Test fork after scope is closed.
+     * Test fork after task scope is closed.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testForkAfterClose(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
+    @Test
+    void testForkAfterClose() {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             scope.close();
             assertThrows(IllegalStateException.class, () -> scope.fork(() -> null));
         }
     }
 
     /**
-     * Test fork when the thread factory rejects creating a thread.
+     * Test fork with a ThreadFactory that rejects creating a thread.
      */
     @Test
-    void testForkRejectedExecutionException() throws Exception {
+    void testForkRejectedExecutionException() {
         ThreadFactory factory = task -> null;
-        try (var scope = new StructuredTaskScope(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             assertThrows(RejectedExecutionException.class, () -> scope.fork(() -> null));
-            scope.join();
         }
     }
 
     /**
      * Test join with no subtasks.
      */
     @Test
     void testJoinWithNoSubtasks() throws Exception {
-        try (var scope = new StructuredTaskScope()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             scope.join();
         }
     }
 
     /**
-     * Test join with unfinished subtasks.
+     * Test join with a remaining subtask.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinWithSubtasks(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
+    void testJoinWithRemainingSubtasks(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             Subtask<String> subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofMillis(50));
+                Thread.sleep(Duration.ofMillis(100));
                 return "foo";
             });
             scope.join();
             assertEquals("foo", subtask.get());
         }
     }
 
     /**
-     * Test join is owner confined.
+     * Test join after join completed with a result.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testJoinConfined(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Boolean>()) {
+    @Test
+    void testJoinAfterJoin1() throws Exception {
+        var results = new LinkedTransferQueue<>(List.of("foo", "bar", "baz"));
+        Joiner<Object, String> joiner = results::take;
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            scope.fork(() -> "foo");
+            assertEquals("foo", scope.join());
 
-            // thread in scope cannot join
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, () -> { scope.join(); });
-                return true;
-            });
+            // join already called
+            for (int i = 0 ; i < 3; i++) {
+                assertThrows(IllegalStateException.class, scope::join);
+            }
+        }
+    }
 
-            scope.join();
+    /**
+     * Test join after join completed with an exception.
+     */
+    @Test
+    void testJoinAfterJoin2() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow())) {
+            scope.fork(() -> { throw new FooException(); });
+            Throwable ex = assertThrows(FailedException.class, scope::join);
+            assertTrue(ex.getCause() instanceof FooException);
 
-            assertTrue(subtask.get());
+            // join already called
+            for (int i = 0 ; i < 3; i++) {
+                assertThrows(IllegalStateException.class, scope::join);
+            }
+        }
+    }
+
+    /**
+     * Test join after join completed with a timeout.
+     */
+    @Test
+    void testJoinAfterJoin3() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow(),
+                cf -> cf.withTimeout(Duration.ofMillis(100)))) {
+            // wait for scope to be cancelled by timeout
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
+            assertThrows(TimeoutException.class, scope::join);
+
+            // join already called
+            for (int i = 0 ; i < 3; i++) {
+                assertThrows(IllegalStateException.class, scope::join);
+            }
+        }
+    }
+
+    /**
+     * Test join method is owner confined.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testJoinConfined(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<Boolean>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             // random thread cannot join
             try (var pool = Executors.newSingleThreadExecutor()) {
                 Future<Void> future = pool.submit(() -> {
                     assertThrows(WrongThreadException.class, scope::join);
                     return null;
                 });
                 future.get();
             }
+
+            // subtask cannot join
+            Subtask<Boolean> subtask = scope.fork(() -> {
+                assertThrows(WrongThreadException.class, () -> { scope.join(); });
+                return true;
+            });
+            scope.join();
+            assertTrue(subtask.get());
         }
     }
 
     /**
      * Test join with interrupt status set.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptJoin1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
-            var latch = new CountDownLatch(1);
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
+                Thread.sleep(60_000);
                 return "foo";
             });
 
             // join should throw
             Thread.currentThread().interrupt();
             try {
                 scope.join();
                 fail("join did not throw");
             } catch (InterruptedException expected) {
-                assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
+                assertFalse(Thread.interrupted());   // interrupt status should be cleared
             }
-
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
         }
     }
 
     /**
      * Test interrupt of thread blocked in join.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptJoin2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+
             var latch = new CountDownLatch(1);
             Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
+                Thread.sleep(60_000);
                 return "foo";
             });
 
-            // join should throw
-            scheduleInterruptAt("java.util.concurrent.StructuredTaskScope.join");
+            // interrupt main thread when it blocks in join
+            scheduleInterruptAt("java.util.concurrent.StructuredTaskScopeImpl.join");
             try {
                 scope.join();
                 fail("join did not throw");
             } catch (InterruptedException expected) {
                 assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
             }
-
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
         }
     }
 
     /**
-     * Test join when scope is shutdown.
+     * Test join when scope is cancelled.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinWithShutdown1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            var interrupted = new CountDownLatch(1);
-            var finish = new CountDownLatch(1);
-
-            Subtask<String> subtask = scope.fork(() -> {
-                try {
-                    Thread.sleep(Duration.ofDays(1));
-                } catch (InterruptedException e) {
-                    interrupted.countDown();
-                }
-                finish.await();
-                return "foo";
-            });
-
-            scope.shutdown();      // should interrupt task
+    void testJoinWhenCancelled(ThreadFactory factory) throws Exception {
+        var countingThreadFactory = new CountingThreadFactory(factory);
+        var testJoiner = new CancelAfterOneJoiner<String>();
 
-            interrupted.await();
-
-            scope.join();
-
-            // signal task to finish
-            finish.countDown();
-        }
-    }
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                    cf -> cf.withThreadFactory(countingThreadFactory))) {
 
-    /**
-     * Test shutdown when owner is blocked in join.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testJoinWithShutdown2(ThreadFactory factory) throws Exception {
-        class MyScope<T> extends StructuredTaskScope<T> {
-            MyScope(ThreadFactory factory) {
-                super(null, factory);
-            }
-            @Override
-            protected void handleComplete(Subtask<? extends T> subtask) {
-                shutdown();
+            // fork subtask, the scope should be cancelled when the subtask completes
+            var subtask1 = scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
             }
-        }
 
-        try (var scope = new MyScope<String>(factory)) {
-            Subtask<String> subtask1 = scope.fork(() -> {
-                Thread.sleep(Duration.ofMillis(50));
-                return "foo";
-            });
-            Subtask<String> subtask2 = scope.fork(() -> {
+            // fork second subtask, it should not run
+            var subtask2 = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return "bar";
             });
 
-            // join should wakeup when shutdown is called
             scope.join();
 
-            // task1 should have completed successfully
-            assertEquals(Subtask.State.SUCCESS, subtask1.state());
             assertEquals("foo", subtask1.get());
-            assertThrows(IllegalStateException.class, subtask1::exception);
-
-            // task2 result/exception not available
             assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
-            assertThrows(IllegalStateException.class, subtask2::get);
-            assertThrows(IllegalStateException.class, subtask2::exception);
         }
     }
 
     /**
      * Test join after scope is closed.
      */
     @Test
     void testJoinAfterClose() throws Exception {
-        try (var scope = new StructuredTaskScope()) {
-            scope.join();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             scope.close();
             assertThrows(IllegalStateException.class, () -> scope.join());
-            assertThrows(IllegalStateException.class, () -> scope.joinUntil(Instant.now()));
         }
     }
 
     /**
-     * Test joinUntil, subtasks finish before deadline expires.
+     * Test join with timeout, subtasks finish before timeout expires.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
+    void testJoinWithTimeout1(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofDays(1)))) {
+
             Subtask<String> subtask = scope.fork(() -> {
-                try {
-                    Thread.sleep(Duration.ofSeconds(2));
-                } catch (InterruptedException e) { }
+                Thread.sleep(Duration.ofSeconds(1));
                 return "foo";
             });
 
-            long startMillis = millisTime();
-            scope.joinUntil(Instant.now().plusSeconds(30));
-            expectDuration(startMillis, /*min*/1900, /*max*/20_000);
+            scope.join();
+
+            assertFalse(scope.isCancelled());
             assertEquals("foo", subtask.get());
         }
     }
 
     /**
-     * Test joinUntil, deadline expires before subtasks finish.
+     * Test join with timeout, timeout expires before subtasks finish.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
+    void testJoinWithTimeout2(ThreadFactory factory) throws Exception {
+        long startMillis = millisTime();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(2)))) {
+
             Subtask<Void> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
 
-            long startMillis = millisTime();
-            try {
-                scope.joinUntil(Instant.now().plusSeconds(2));
-            } catch (TimeoutException e) {
-                expectDuration(startMillis, /*min*/1900, /*max*/20_000);
-            }
+            assertThrows(TimeoutException.class, scope::join);
+            expectDuration(startMillis, /*min*/1900, /*max*/20_000);
+
+            assertTrue(scope.isCancelled());
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test joinUntil many times.
+     * Test join with timeout that has already expired.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil3(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            Subtask<String> subtask = scope.fork(() -> {
+    void testJoinWithTimeout3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(-1)))) {
+
+            Subtask<Void> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
 
-            for (int i = 0; i < 3; i++) {
-                try {
-                    scope.joinUntil(Instant.now().plusMillis(50));
-                    fail("joinUntil did not throw");
-                } catch (TimeoutException expected) {
-                    assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-                }
-            }
+            assertThrows(TimeoutException.class, scope::join);
+
+            assertTrue(scope.isCancelled());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test joinUntil with a deadline that has already expired.
+     * Test that cancelling execution interrupts unfinished threads. This test uses
+     * a custom Joiner to cancel execution.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil4(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            Subtask<Void> subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
+    void testCancelInterruptsThreads2(ThreadFactory factory) throws Exception {
+        var testJoiner = new CancelAfterOneJoiner<String>();
+
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(factory))) {
+
+            // fork subtask1 that runs for a long time
+            var started = new CountDownLatch(1);
+            var interrupted = new CountDownLatch(1);
+            var subtask1 = scope.fork(() -> {
+                started.countDown();
+                try {
+                    Thread.sleep(Duration.ofDays(1));
+                } catch (InterruptedException e) {
+                    interrupted.countDown();
+                }
             });
+            started.await();
 
-            // now
-            try {
-                scope.joinUntil(Instant.now());
-                fail("joinUntil did not throw");
-            } catch (TimeoutException expected) {
-                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
+            // fork subtask2, the scope should be cancelled when the subtask completes
+            var subtask2 = scope.fork(() -> "bar");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
             }
 
-            // in the past
-            try {
-                scope.joinUntil(Instant.now().minusSeconds(1));
-                fail("joinUntil did not throw");
-            } catch (TimeoutException expected) {
-                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            }
+            // subtask1 should be interrupted
+            interrupted.await();
+
+            scope.join();
+            assertEquals(Subtask.State.UNAVAILABLE, subtask1.state());
+            assertEquals("bar", subtask2.get());
         }
     }
 
     /**
-     * Test joinUntil with interrupt status set.
+     * Test that timeout interrupts unfinished threads.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testInterruptJoinUntil1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            var latch = new CountDownLatch(1);
+    void testTimeoutInterruptsThreads(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(2)))) {
 
-            Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
-                return "foo";
+            var started = new AtomicBoolean();
+            var interrupted = new CountDownLatch(1);
+            Subtask<Void> subtask = scope.fork(() -> {
+                started.set(true);
+                try {
+                    Thread.sleep(Duration.ofDays(1));
+                } catch (InterruptedException e) {
+                    interrupted.countDown();
+                }
+                return null;
             });
 
-            // joinUntil should throw
-            Thread.currentThread().interrupt();
-            try {
-                scope.joinUntil(Instant.now().plusSeconds(30));
-                fail("joinUntil did not throw");
-            } catch (InterruptedException expected) {
-                assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
+            while (!scope.isCancelled()) {
+                Thread.sleep(50);
             }
 
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
+            // if subtask started then it should be interrupted
+            if (started.get()) {
+                interrupted.await();
+            }
+
+            assertThrows(TimeoutException.class, scope::join);
+
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test interrupt of thread blocked in joinUntil.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testInterruptJoinUntil2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
-            var latch = new CountDownLatch(1);
-
-            Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
-                return "foo";
-            });
-
-            // joinUntil should throw
-            scheduleInterruptAt("java.util.concurrent.StructuredTaskScope.joinUntil");
-            try {
-                scope.joinUntil(Instant.now().plusSeconds(30));
-                fail("joinUntil did not throw");
-            } catch (InterruptedException expected) {
-                assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
-            }
-
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
-        }
-    }
-
-    /**
-     * Test that shutdown interrupts unfinished subtasks.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownInterruptsThreads1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var interrupted = new AtomicBoolean();
-            var latch = new CountDownLatch(1);
-            var subtask = scope.fork(() -> {
-                try {
-                    Thread.sleep(Duration.ofDays(1));
-                } catch (InterruptedException e) {
-                    interrupted.set(true);
-                } finally {
-                    latch.countDown();
-                }
-                return null;
-            });
-
-            scope.shutdown();
-
-            // wait for task to complete
-            latch.await();
-            assertTrue(interrupted.get());
-
-            scope.join();
-
-            // subtask result/exception not available
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalStateException.class, subtask::get);
-            assertThrows(IllegalStateException.class, subtask::exception);
-        }
-    }
-
-    /**
-     * Test that shutdown does not interrupt current thread.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownInterruptsThreads2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var interrupted = new AtomicBoolean();
-            var latch = new CountDownLatch(1);
-            var subtask = scope.fork(() -> {
-                try {
-                    scope.shutdown();
-                    interrupted.set(Thread.currentThread().isInterrupted());
-                } finally {
-                    latch.countDown();
-                }
-                return null;
-            });
-
-            // wait for task to complete
-            latch.await();
-            assertFalse(interrupted.get());
-
-            scope.join();
-        }
-    }
-
-    /**
-     * Test shutdown wakes join.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownWakesJoin(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var latch = new CountDownLatch(1);
-            scope.fork(() -> {
-                Thread.sleep(Duration.ofMillis(100));  // give time for join to block
-                scope.shutdown();
-                latch.await();
-                return null;
-            });
-
-            scope.join();
-
-            // join woke up, allow task to complete
-            latch.countDown();
-        }
-    }
-
-    /**
-     * Test shutdown after scope is closed.
-     */
-    @Test
-    void testShutdownAfterClose() throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            scope.join();
-            scope.close();
-            assertThrows(IllegalStateException.class, scope::shutdown);
-        }
-    }
-
-    /**
-     * Test shutdown is confined to threads in the scope "tree".
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownConfined(ThreadFactory factory) throws Exception {
-        try (var scope1 = new StructuredTaskScope<Boolean>();
-             var scope2 = new StructuredTaskScope<Boolean>()) {
-
-            // thread in scope1 cannot shutdown scope2
-            Subtask<Boolean> subtask1 = scope1.fork(() -> {
-                assertThrows(WrongThreadException.class, scope2::shutdown);
-                return true;
-            });
-
-            // wait for task in scope1 to complete to avoid racing with task in scope2
-            while (subtask1.state() == Subtask.State.UNAVAILABLE) {
-                Thread.sleep(10);
-            }
-
-            // thread in scope2 shutdown scope1
-            Subtask<Boolean> subtask2 = scope2.fork(() -> {
-                scope1.shutdown();
-                return true;
-            });
-
-            scope2.join();
-            scope1.join();
-
-            assertTrue(subtask1.get());
-            assertTrue(subtask1.get());
-
-            // random thread cannot shutdown
-            try (var pool = Executors.newSingleThreadExecutor()) {
-                Future<Void> future = pool.submit(() -> {
-                    assertThrows(WrongThreadException.class, scope1::shutdown);
-                    assertThrows(WrongThreadException.class, scope2::shutdown);
-                    return null;
-                });
-                future.get();
-            }
-        }
-    }
-
-    /**
-     * Test isShutdown.
-     */
-    @Test
-    void testIsShutdown() {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            assertFalse(scope.isShutdown());   // before shutdown
-            scope.shutdown();
-            assertTrue(scope.isShutdown());    // after shutdown
-            scope.close();
-            assertTrue(scope.isShutdown());    // after cose
-        }
-    }
-
-    /**
-     * Test close without join, no subtasks forked.
+     * Test close without join, no subtasks forked.
      */
     @Test
     void testCloseWithoutJoin1() {
-        try (var scope = new StructuredTaskScope<Object>()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             // do nothing
         }
     }
 
     /**
-     * Test close without join, unfinished subtasks.
+     * Test close without join, subtasks forked.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testCloseWithoutJoin2(ThreadFactory factory) {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             Subtask<String> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
-            assertThrows(IllegalStateException.class, scope::close);
-
-            // subtask result/exception not available
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalStateException.class, subtask::get);
-            assertThrows(IllegalStateException.class, subtask::exception);
-        }
-    }
 
-    /**
-     * Test close without join, unfinished subtasks forked after join.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testCloseWithoutJoin3(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
-            scope.fork(() -> "foo");
-            scope.join();
-
-            Subtask<String> subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            });
+            // first call to close should throw
             assertThrows(IllegalStateException.class, scope::close);
 
+            // subsequent calls to close should not throw
+            for (int i = 0; i < 3; i++) {
+                scope.close();
+            }
+
             // subtask result/exception not available
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
         }
@@ -885,89 +708,85 @@
      * Test close after join throws. Close should not throw as join attempted.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testCloseAfterJoinThrows(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             var subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
 
             // join throws
             Thread.currentThread().interrupt();
             assertThrows(InterruptedException.class, scope::join);
             assertThrows(IllegalStateException.class, subtask::get);
-        }
-    }
-
-    /**
-     * Test close after joinUntil throws. Close should not throw as join attempted.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testCloseAfterJoinUntilThrows(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            var subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            });
 
-            // joinUntil throws
-            assertThrows(TimeoutException.class, () -> scope.joinUntil(Instant.now()));
-            assertThrows(IllegalStateException.class, subtask::get);
-        }
+        }  // close should not throw
     }
 
     /**
-     * Test close is owner confined.
+     * Test close method is owner confined.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testCloseConfined(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Boolean>()) {
-
-            // attempt to close from thread in scope
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, scope::close);
-                return true;
-            });
-
-            scope.join();
-            assertTrue(subtask.get());
+        try (var scope = StructuredTaskScope.open(Joiner.<Boolean>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             // random thread cannot close scope
             try (var pool = Executors.newCachedThreadPool(factory)) {
                 Future<Boolean> future = pool.submit(() -> {
                     assertThrows(WrongThreadException.class, scope::close);
                     return null;
                 });
                 future.get();
             }
+
+            // subtask cannot close
+            Subtask<Boolean> subtask = scope.fork(() -> {
+                assertThrows(WrongThreadException.class, scope::close);
+                return true;
+            });
+            scope.join();
+            assertTrue(subtask.get());
         }
     }
 
     /**
      * Test close with interrupt status set.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptClose1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
+        var testJoiner = new CancelAfterOneJoiner<String>();
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(factory))) {
+
+            // fork first subtask, a straggler as it continues after being interrupted
+            var started = new CountDownLatch(1);
             var done = new AtomicBoolean();
             scope.fork(() -> {
+                started.countDown();
                 try {
                     Thread.sleep(Duration.ofDays(1));
                 } catch (InterruptedException e) {
-                    // interrupted by shutdown, expected
+                    // interrupted by cancel, expected
                 }
                 Thread.sleep(Duration.ofMillis(100)); // force close to wait
                 done.set(true);
                 return null;
             });
+            started.await();
+
+            // fork second subtask, the scope should be cancelled when this subtask completes
+            scope.fork(() -> "bar");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
 
-            scope.shutdown();
             scope.join();
 
             // invoke close with interrupt status set
             Thread.currentThread().interrupt();
             try {
@@ -983,285 +802,198 @@
      * Test interrupting thread waiting in close.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptClose2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var done = new AtomicBoolean();
+        var testJoiner = new CancelAfterOneJoiner<String>();
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(factory))) {
+
             Thread mainThread = Thread.currentThread();
+
+            // fork first subtask, a straggler as it continues after being interrupted
+            var started = new CountDownLatch(1);
+            var done = new AtomicBoolean();
             scope.fork(() -> {
+                started.countDown();
                 try {
                     Thread.sleep(Duration.ofDays(1));
                 } catch (InterruptedException e) {
-                    // interrupted by shutdown, expected
+                    // interrupted by cancel, expected
                 }
 
                 // interrupt main thread when it blocks in close
-                interruptThreadAt(mainThread, "java.util.concurrent.StructuredTaskScope.close");
+                interruptThreadAt(mainThread, "java.util.concurrent.StructuredTaskScopeImpl.close");
 
                 Thread.sleep(Duration.ofMillis(100)); // force close to wait
                 done.set(true);
                 return null;
             });
+            started.await();
+
+            // fork second subtask, the scope should be cancelled when this subtask completes
+            scope.fork(() -> "bar");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
 
-            scope.shutdown();   // interrupts task
             scope.join();
+
+            // main thread will be interrupted while blocked in close
             try {
                 scope.close();
             } finally {
-                assertTrue(Thread.interrupted()); // clear interrupt status
+                assertTrue(Thread.interrupted());   // clear interrupt status
                 assertTrue(done.get());
             }
         }
     }
 
     /**
      * Test that closing an enclosing scope closes the thread flock of a nested scope.
      */
     @Test
     void testCloseThrowsStructureViolation() throws Exception {
-        try (var scope1 = new StructuredTaskScope<Object>()) {
-            try (var scope2 = new StructuredTaskScope<Object>()) {
+        try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll())) {
+            try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll())) {
 
-                // join + close enclosing scope
-                scope1.join();
+                // close enclosing scope
                 try {
                     scope1.close();
                     fail("close did not throw");
                 } catch (StructureViolationException expected) { }
 
-                // underlying flock should be closed, fork should return a cancelled task
+                // underlying flock should be closed
                 var executed = new AtomicBoolean();
-                Subtask<Void> subtask = scope2.fork(() -> {
-                    executed.set(true);
-                    return null;
-                });
+                Subtask<?> subtask = scope2.fork(() -> executed.set(true));
                 assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
                 scope2.join();
                 assertFalse(executed.get());
+                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             }
         }
     }
 
     /**
-     * A StructuredTaskScope that collects the subtasks notified to the handleComplete method.
+     * Test that isCancelled returns true after close.
      */
-    private static class CollectAll<T> extends StructuredTaskScope<T> {
-        private final Set<Subtask<? extends T>> subtasks = ConcurrentHashMap.newKeySet();
-
-        CollectAll(ThreadFactory factory) {
-            super(null, factory);
-        }
-
-        @Override
-        protected void handleComplete(Subtask<? extends T> subtask) {
-            subtasks.add(subtask);
-        }
-
-        Set<Subtask<? extends T>> subtasks() {
-            return subtasks;
-        }
-
-        Subtask<? extends T> find(Callable<T> task) {
-            return subtasks.stream()
-                    .filter(h -> task.equals(h.task()))
-                    .findAny()
-                    .orElseThrow();
-        }
-    }
-
-    /**
-     * Test that handleComplete method is invoked for tasks that complete before shutdown.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testHandleCompleteBeforeShutdown(ThreadFactory factory) throws Exception {
-        try (var scope = new CollectAll<String>(factory)) {
-            Callable<String> task1 = () -> "foo";
-            Callable<String> task2 = () -> { throw new FooException(); };
-            scope.fork(task1);
-            scope.fork(task2);
-            scope.join();
-
-            var subtask1 = scope.find(task1);
-            assertEquals("foo", subtask1.get());
-
-            var subtask2 = scope.find(task2);
-            assertTrue(subtask2.exception() instanceof FooException);
+    @Test
+    void testIsCancelledAfterClose() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            assertFalse(scope.isCancelled());
+            scope.close();
+            assertTrue(scope.isCancelled());
         }
     }
 
     /**
-     * Test that handleComplete method is not invoked for tasks that finish after shutdown
-     * or are forked after shutdown.
+     * Test Joiner.onFork throwing exception.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testHandleCompleteAfterShutdown(ThreadFactory factory) throws Exception {
-        try (var scope = new CollectAll<String>(factory)) {
-            Callable<String> task1 = () -> {
-                try {
-                    Thread.sleep(Duration.ofDays(1));
-                } catch (InterruptedException ignore) { }
-                return "foo";
-            };
-            Callable<String> task2 = () -> {
-                Thread.sleep(Duration.ofDays(1));
-                return "bar";
-            };
-            Callable<String> task3 = () -> "baz";
-
-            // forked before shutdown, will complete after shutdown
-            var subtask1 = scope.fork(task1);
-            var subtask2 = scope.fork(task2);
-
-            scope.shutdown();
-
-            // forked after shutdown
-            var subtask3 = scope.fork(task3);
-
-            scope.join();
-
-            // handleComplete should not be called
-            for (int i = 0; i < 3; i++) {
-                assertEquals(0, scope.subtasks().size());
-                Thread.sleep(20);
+    @Test
+    void testOnForkThrows() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onFork(Subtask<? extends String> subtask) {
+                throw new FooException();
             }
-
-            assertEquals(Subtask.State.UNAVAILABLE, subtask1.state());
-            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
-            assertEquals(Subtask.State.UNAVAILABLE, subtask3.state());
+            @Override
+            public Void result() {
+                return null;
+            }
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertThrows(FooException.class, () -> scope.fork(() -> "foo"));
         }
     }
 
     /**
-     * Test that the default handleComplete throws IllegalArgumentException if called
-     * with a running task.
+     * Test Joiner.onFork returning true to cancel execution.
      */
     @Test
-    void testHandleCompleteThrows() throws Exception {
-        class TestScope<T> extends StructuredTaskScope<T> {
-            protected void handleComplete(Subtask<? extends T> subtask) {
-                super.handleComplete(subtask);
+    void testOnForkCancelsExecution() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onFork(Subtask<? extends String> subtask) {
+                return true;
             }
-        }
-
-        try (var scope = new TestScope<String>()) {
-            var subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return "foo";
-            });
-
-            // running task
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalArgumentException.class, () -> scope.handleComplete(subtask));
-            scope.shutdown();
-
-            // null task
-            assertThrows(NullPointerException.class, () -> scope.handleComplete(null));
-
+            @Override
+            public Void result() {
+                return null;
+            }
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertFalse(scope.isCancelled());
+            scope.fork(() -> "foo");
+            assertTrue(scope.isCancelled());
             scope.join();
         }
     }
 
     /**
-     * Test ensureOwnerAndJoined.
+     * Test Joiner.onComplete throwing exception causes UHE to be invoked.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testEnsureOwnerAndJoined(ThreadFactory factory) throws Exception {
-        class MyScope<T> extends StructuredTaskScope<T> {
-            MyScope(ThreadFactory factory) {
-                super(null, factory);
-            }
-            void invokeEnsureOwnerAndJoined() {
-                super.ensureOwnerAndJoined();
+    @Test
+    void testOnCompleteThrows() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onComplete(Subtask<? extends String> subtask) {
+                throw new FooException();
             }
-        }
-
-        try (var scope = new MyScope<Boolean>(factory)) {
-            // owner thread, before join
-            scope.fork(() -> true);
-            assertThrows(IllegalStateException.class, () -> {
-                scope.invokeEnsureOwnerAndJoined();
-            });
-
-            // owner thread, after join
-            scope.join();
-            scope.invokeEnsureOwnerAndJoined();
-
-            // thread in scope cannot invoke ensureOwnerAndJoined
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, () -> {
-                    scope.invokeEnsureOwnerAndJoined();
-                });
-                return true;
-            });
-            scope.join();
-            assertTrue(subtask.get());
-
-            // random thread cannot invoke ensureOwnerAndJoined
-            try (var pool = Executors.newSingleThreadExecutor()) {
-                Future<Void> future = pool.submit(() -> {
-                    assertThrows(WrongThreadException.class, () -> {
-                        scope.invokeEnsureOwnerAndJoined();
-                    });
-                    return null;
-                });
-                future.get();
+            @Override
+            public Void result() {
+                return null;
             }
+        };
+        var excRef = new AtomicReference<Throwable>();
+        Thread.UncaughtExceptionHandler uhe = (t, e) -> excRef.set(e);
+        ThreadFactory factory = Thread.ofVirtual()
+                .uncaughtExceptionHandler(uhe)
+                .factory();
+        try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> "foo");
+            scope.join();
+            assertInstanceOf(FooException.class, excRef.get());
         }
     }
 
     /**
-     * Test ensureOwnerAndJoined after the task scope has been closed.
+     * Test Joiner.onComplete returning true to cancel execution.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testEnsureOwnerAndJoinedAfterClose(ThreadFactory factory) throws Exception {
-        class MyScope<T> extends StructuredTaskScope<T> {
-            MyScope(ThreadFactory factory) {
-                super(null, factory);
+    @Test
+    void testOnCompleteCancelsExecution() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onComplete(Subtask<? extends String> subtask) {
+                return true;
             }
-            public void invokeEnsureOwnerAndJoined() {
-                super.ensureOwnerAndJoined();
+            @Override
+            public Void result() {
+                return null;
             }
-        }
-
-        // ensureOwnerAndJoined after close, join invoked
-        try (var scope = new MyScope<String>(factory)) {
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertFalse(scope.isCancelled());
             scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(10);
+            }
             scope.join();
-            scope.close();
-            scope.invokeEnsureOwnerAndJoined();  // should not throw
-        }
-
-        // ensureOwnerAndJoined after close, join not invoked
-        try (var scope = new MyScope<String>(factory)) {
-            scope.fork(() -> "foo");
-            assertThrows(IllegalStateException.class, scope::close);
-            scope.invokeEnsureOwnerAndJoined();  // should not throw
         }
     }
 
-
     /**
      * Test toString.
      */
     @Test
     void testToString() throws Exception {
-        ThreadFactory factory = Thread.ofVirtual().factory();
-        try (var scope = new StructuredTaskScope<Object>("duke", factory)) {
-            // open
-            assertTrue(scope.toString().contains("duke"));
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withName("duke"))) {
 
-            // shutdown
-            scope.shutdown();
+            // open
             assertTrue(scope.toString().contains("duke"));
 
             // closed
-            scope.join();
             scope.close();
             assertTrue(scope.toString().contains("duke"));
         }
     }
 
@@ -1269,23 +1001,22 @@
      * Test Subtask with task that completes successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testSubtaskWhenSuccess(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            Callable<String> task = () -> "foo";
-            Subtask<String> subtask = scope.fork(task);
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+
+            Subtask<String> subtask = scope.fork(() -> "foo");
 
-            // before join, owner thread
-            assertEquals(task, subtask.task());
+            // before join
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
 
             scope.join();
 
             // after join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.SUCCESS, subtask.state());
             assertEquals("foo", subtask.get());
             assertThrows(IllegalStateException.class, subtask::exception);
         }
     }
@@ -1294,23 +1025,22 @@
      * Test Subtask with task that fails.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testSubtaskWhenFailed(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            Callable<String> task = () -> { throw new FooException(); };
-            Subtask<String> subtask = scope.fork(task);
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+
+            Subtask<String> subtask = scope.fork(() -> { throw new FooException(); });
 
-            // before join, owner thread
-            assertEquals(task, subtask.task());
+            // before join
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
 
             scope.join();
 
             // after join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.FAILED, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertTrue(subtask.exception() instanceof FooException);
         }
     }
@@ -1319,53 +1049,55 @@
      * Test Subtask with a task that has not completed.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testSubtaskWhenNotCompleted(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            Callable<Void> task = () -> {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            Subtask<Void> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
-            };
-            Subtask<Void> subtask = scope.fork(task);
+            });
 
             // before join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
 
             // attempt join, join throws
             Thread.currentThread().interrupt();
             assertThrows(InterruptedException.class, scope::join);
 
             // after join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
         }
     }
 
     /**
-     * Test Subtask when forked after shutdown.
+     * Test Subtask forked after execution cancelled.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testSubtaskWhenShutdown(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            Callable<Void> task = () -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            };
+    void testSubtaskWhenCancelled(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(new CancelAfterOneJoiner<String>())) {
+            scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
 
-            scope.shutdown();
+            var subtask = scope.fork(() -> "foo");
+
+            // before join
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
+            assertThrows(IllegalStateException.class, subtask::get);
+            assertThrows(IllegalStateException.class, subtask::exception);
 
-            // fork after shutdown
-            Subtask<Void> subtask = scope.fork(task);
             scope.join();
-            assertEquals(task, subtask.task());
+
+            // after join
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
         }
     }
@@ -1373,251 +1105,598 @@
     /**
      * Test Subtask::toString.
      */
     @Test
     void testSubtaskToString() throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            // success
-            var subtask1 = scope.fork(() -> "foo");
-            scope.join();
-            assertTrue(subtask1.toString().contains("Completed successfully"));
-
-            // failed
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            var latch = new CountDownLatch(1);
+            var subtask1 = scope.fork(() -> {
+                latch.await();
+                return "foo";
+            });
             var subtask2 = scope.fork(() -> { throw new FooException(); });
+
+            // subtask1 result is unavailable
+            assertTrue(subtask1.toString().contains("Unavailable"));
+            latch.countDown();
+
             scope.join();
-            assertTrue(subtask2.toString().contains("Failed"));
 
-            // not completed
-            Callable<Void> sleepForDay = () -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            };
-            var subtask3 = scope.fork(sleepForDay);
-            assertTrue(subtask3.toString().contains("Unavailable"));
+            assertTrue(subtask1.toString().contains("Completed successfully"));
+            assertTrue(subtask2.toString().contains("Failed"));
+        }
+    }
 
-            scope.shutdown();
+    /**
+     * Test Joiner.allSuccessfulOrThrow() with no subtasks.
+     */
+    @Test
+    void testAllSuccessfulOrThrow1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
+            var subtasks = scope.join().toList();
+            assertTrue(subtasks.isEmpty());
+        }
+    }
 
-            // forked after shutdown
-            var subtask4 = scope.fork(sleepForDay);
-            assertTrue(subtask4.toString().contains("Unavailable"));
+    /**
+     * Test Joiner.allSuccessfulOrThrow() with subtasks that complete successfully.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllSuccessfulOrThrow2(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> "bar");
+            var subtasks = scope.join().toList();
+            assertEquals(List.of(subtask1, subtask2), subtasks);
+            assertEquals("foo", subtask1.get());
+            assertEquals("bar", subtask2.get());
+        }
+    }
 
-            scope.join();
+    /**
+     * Test Joiner.allSuccessfulOrThrow() with a subtask that complete successfully and
+     * a subtask that fails.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllSuccessfulOrThrow3(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> "foo");
+            scope.fork(() -> { throw new FooException(); });
+            try {
+                scope.join();
+            } catch (FailedException e) {
+                assertTrue(e.getCause() instanceof FooException);
+            }
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with no completed tasks.
+     * Test Joiner.anySuccessfulResultOrThrow() with no subtasks.
      */
     @Test
-    void testShutdownOnSuccess1() throws Exception {
-        try (var scope = new ShutdownOnSuccess<Object>()) {
-            assertThrows(IllegalStateException.class, () -> scope.result());
-            assertThrows(IllegalStateException.class, () -> scope.result(e -> null));
+    void testAnySuccessfulResultOrThrow1() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow())) {
+            try {
+                scope.join();
+            } catch (FailedException e) {
+                assertTrue(e.getCause() instanceof NoSuchElementException);
+            }
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with tasks that complete successfully.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that completes successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess2(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<String>(null, factory)) {
+    void testAnySuccessfulResultOrThrow2(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.fork(() -> "foo");
-            scope.join();  // ensures foo completes first
-            scope.fork(() -> "bar");
-            scope.join();
-            assertEquals("foo", scope.result());
-            assertEquals("foo", scope.result(e -> null));
+            String result = scope.join();
+            assertEquals("foo", result);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with a task that completes successfully with a null result.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that completes successfully
+     * with a null result.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess3(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<Object>(null, factory)) {
+    void testAnySuccessfulResultOrThrow3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.fork(() -> null);
-            scope.join();
-            assertNull(scope.result());
-            assertNull(scope.result(e -> null));
+            String result = scope.join();
+            assertNull(result);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with tasks that complete succcessfully and tasks that fail.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that complete succcessfully
+     * and a subtask that fails.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess4(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<String>(null, factory)) {
+    void testAnySuccessfulResultOrThrow4(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.fork(() -> "foo");
-            scope.fork(() -> { throw new ArithmeticException(); });
-            scope.join();
-            assertEquals("foo", scope.result());
-            assertEquals("foo", scope.result(e -> null));
+            scope.fork(() -> { throw new FooException(); });
+            String first = scope.join();
+            assertEquals("foo", first);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with a task that fails.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that fails.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess5(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<Object>(null, factory)) {
-            scope.fork(() -> { throw new ArithmeticException(); });
-            scope.join();
-            Throwable ex = assertThrows(ExecutionException.class, () -> scope.result());
-            assertTrue(ex.getCause() instanceof ArithmeticException);
-            ex = assertThrows(FooException.class, () -> scope.result(e -> new FooException(e)));
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+    void testAnySuccessfulResultOrThrow5(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> { throw new FooException(); });
+            Throwable ex = assertThrows(FailedException.class, scope::join);
+            assertTrue(ex.getCause() instanceof FooException);
+        }
+    }
+
+    /**
+     * Test Joiner.awaitAllSuccessfulOrThrow() with no subtasks.
+     */
+    @Test
+    void testAwaitSuccessfulOrThrow1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow())) {
+            var result = scope.join();
+            assertNull(result);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess methods are confined to the owner.
+     * Test Joiner.awaitAllSuccessfulOrThrow() with subtasks that complete successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccessConfined(ThreadFactory factory) throws Exception {
-        // owner before join
-        try (var scope = new ShutdownOnSuccess<Boolean>(null, factory)) {
-            scope.fork(() -> { throw new FooException(); });
-            assertThrows(IllegalStateException.class, scope::result);
-            assertThrows(IllegalStateException.class, () -> {
-                scope.result(e -> new RuntimeException(e));
-            });
-            scope.join();
+    void testAwaitSuccessfulOrThrow2(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> "bar");
+            var result = scope.join();
+            assertNull(result);
+            assertEquals("foo", subtask1.get());
+            assertEquals("bar", subtask2.get());
         }
+    }
 
-        // non-owner
-        try (var scope = new ShutdownOnSuccess<Boolean>(null, factory)) {
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, scope::result);
-                assertThrows(WrongThreadException.class, () -> {
-                    scope.result(e -> new RuntimeException(e));
-                });
-                return true;
-            });
-            scope.join();
-            assertTrue(subtask.get());
+    /**
+     * Test Joiner.awaitAllSuccessfulOrThrow() with a subtask that complete successfully and
+     * a subtask that fails.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAwaitSuccessfulOrThrow3(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> "foo");
+            scope.fork(() -> { throw new FooException(); });
+            try {
+                scope.join();
+            } catch (FailedException e) {
+                assertTrue(e.getCause() instanceof FooException);
+            }
         }
     }
 
     /**
-     * Test ShutdownOnFailure with no completed tasks.
+     * Test Joiner.awaitAll() with no subtasks.
      */
     @Test
-    void testShutdownOnFailure1() throws Throwable {
-        try (var scope = new ShutdownOnFailure()) {
-            assertTrue(scope.exception().isEmpty());
-            scope.throwIfFailed();
-            scope.throwIfFailed(e -> new FooException(e));
+    void testAwaitAll1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            var result = scope.join();
+            assertNull(result);
         }
     }
 
     /**
-     * Test ShutdownOnFailure with tasks that complete successfully.
+     * Test Joiner.awaitAll() with subtasks that complete successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnFailure2(ThreadFactory factory) throws Throwable {
-        try (var scope = new ShutdownOnFailure(null, factory)) {
-            scope.fork(() -> "foo");
-            scope.fork(() -> "bar");
-            scope.join();
+    void testAwaitAll2(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> "bar");
+            var result = scope.join();
+            assertNull(result);
+            assertEquals("foo", subtask1.get());
+            assertEquals("bar", subtask2.get());
+        }
+    }
+
+    /**
+     * Test Joiner.awaitAll() with a subtask that complete successfully and a subtask
+     * that fails.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAwaitAll3(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> { throw new FooException(); });
+            var result = scope.join();
+            assertNull(result);
+            assertEquals("foo", subtask1.get());
+            assertTrue(subtask2.exception() instanceof FooException);
+        }
+    }
 
-            // no exception
-            assertTrue(scope.exception().isEmpty());
-            scope.throwIfFailed();
-            scope.throwIfFailed(e -> new FooException(e));
+    /**
+     * Test Joiner.allUntil(Predicate) with no subtasks.
+     */
+    @Test
+    void testAllUntil1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.allUntil(s -> false))) {
+            var subtasks = scope.join();
+            assertEquals(0, subtasks.count());
         }
     }
 
     /**
-     * Test ShutdownOnFailure with tasks that complete succcessfully and tasks that fail.
+     * Test Joiner.allUntil(Predicate) with no cancellation.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnFailure3(ThreadFactory factory) throws Throwable {
-        try (var scope = new ShutdownOnFailure(null, factory)) {
+    void testAllUntil2(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allUntil(s -> false),
+                cf -> cf.withThreadFactory(factory))) {
 
-            // one task completes successfully, the other fails
-            scope.fork(() -> "foo");
-            scope.fork(() -> { throw new ArithmeticException(); });
-            scope.join();
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> { throw new FooException(); });
+
+            var subtasks = scope.join().toList();
+            assertEquals(2, subtasks.size());
+
+            assertTrue(subtasks.get(0) == subtask1);
+            assertTrue(subtasks.get(1) == subtask2);
+            assertEquals("foo", subtask1.get());
+            assertTrue(subtask2.exception() instanceof FooException);
+        }
+    }
 
-            Throwable ex = scope.exception().orElse(null);
-            assertTrue(ex instanceof ArithmeticException);
+    /**
+     * Test Joiner.allUntil(Predicate) with cancellation after one subtask completes.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllUntil3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allUntil(s -> true),
+                cf -> cf.withThreadFactory(factory))) {
+
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> {
+                Thread.sleep(Duration.ofDays(1));
+                return "bar";
+            });
 
-            ex = assertThrows(ExecutionException.class, () -> scope.throwIfFailed());
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+            var subtasks = scope.join().toList();
 
-            ex = assertThrows(FooException.class,
-                              () -> scope.throwIfFailed(e -> new FooException(e)));
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+            assertEquals(2, subtasks.size());
+            assertTrue(subtasks.get(0) == subtask1);
+            assertTrue(subtasks.get(1) == subtask2);
+            assertEquals("foo", subtask1.get());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
         }
     }
 
     /**
-     * Test ShutdownOnFailure methods are confined to the owner.
+     * Test Joiner.allUntil(Predicate) with cancellation after serveral subtasks complete.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnFailureConfined(ThreadFactory factory) throws Exception {
-        // owner before join
-        try (var scope = new ShutdownOnFailure(null, factory)) {
+    void testAllUntil4(ThreadFactory factory) throws Exception {
+
+        // cancel execution after two or more failures
+        class CancelAfterTwoFailures<T> implements Predicate<Subtask<? extends T>> {
+            final AtomicInteger failedCount = new AtomicInteger();
+            @Override
+            public boolean test(Subtask<? extends T> subtask) {
+                return subtask.state() == Subtask.State.FAILED
+                        && failedCount.incrementAndGet() >= 2;
+            }
+        }
+        var joiner = Joiner.allUntil(new CancelAfterTwoFailures<String>());
+
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            int forkCount = 0;
+
+            // fork subtasks until execution cancelled
+            while (!scope.isCancelled()) {
+                scope.fork(() -> "foo");
+                scope.fork(() -> { throw new FooException(); });
+                forkCount += 2;
+                Thread.sleep(Duration.ofMillis(10));
+            }
+
+            var subtasks = scope.join().toList();
+            assertEquals(forkCount, subtasks.size());
+
+            long failedCount = subtasks.stream()
+                    .filter(s -> s.state() == Subtask.State.FAILED)
+                    .count();
+            assertTrue(failedCount >= 2);
+        }
+    }
+
+    /**
+     * Test Test Joiner.allUntil(Predicate) where the Predicate's test method throws.
+     */
+    @Test
+    void testAllUntil5() throws Exception {
+        var joiner = Joiner.allUntil(_ -> { throw new FooException(); });
+        var excRef = new AtomicReference<Throwable>();
+        Thread.UncaughtExceptionHandler uhe = (t, e) -> excRef.set(e);
+        ThreadFactory factory = Thread.ofVirtual()
+                .uncaughtExceptionHandler(uhe)
+                .factory();
+        try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) {
             scope.fork(() -> "foo");
-            assertThrows(IllegalStateException.class, scope::exception);
-            assertThrows(IllegalStateException.class, scope::throwIfFailed);
-            assertThrows(IllegalStateException.class, () -> {
-                scope.throwIfFailed(e -> new RuntimeException(e));
-            });
             scope.join();
+            assertInstanceOf(FooException.class, excRef.get());
         }
+    }
 
-        // non-owner
-        try (var scope = new ShutdownOnFailure(null, factory)) {
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, scope::exception);
-                assertThrows(WrongThreadException.class, scope::throwIfFailed);
-                assertThrows(WrongThreadException.class, () -> {
-                    scope.throwIfFailed(e -> new RuntimeException(e));
-                });
-                return true;
+    /**
+     * Test Joiner default methods.
+     */
+    @Test
+    void testJoinerDefaultMethods() throws Exception {
+        try (var scope = StructuredTaskScope.open(new CancelAfterOneJoiner<String>())) {
+
+            // need subtasks to test default methods
+            var subtask1 = scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
+            var subtask2 = scope.fork(() -> "bar");
+            scope.join();
+
+            assertEquals(Subtask.State.SUCCESS, subtask1.state());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
+
+            // Joiner that does not override default methods
+            Joiner<Object, Void> joiner = () -> null;
+            assertThrows(NullPointerException.class, () -> joiner.onFork(null));
+            assertThrows(NullPointerException.class, () -> joiner.onComplete(null));
+            assertThrows(IllegalArgumentException.class, () -> joiner.onFork(subtask1));
+            assertFalse(joiner.onFork(subtask2));
+            assertFalse(joiner.onComplete(subtask1));
+            assertThrows(IllegalArgumentException.class, () -> joiner.onComplete(subtask2));
+        }
+    }
+
+    /**
+     * Test Joiners onFork/onComplete methods with a subtask in an unexpected state.
+     */
+    @Test
+    void testJoinersWithUnavailableResukt() throws Exception {
+        try (var scope = StructuredTaskScope.open()) {
+            var done = new CountDownLatch(1);
+            var subtask = scope.fork(() -> {
+                done.await();
+                return null;
             });
+
+            // onComplete with uncompleted task should throw IAE
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.allSuccessfulOrThrow().onComplete(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.anySuccessfulResultOrThrow().onComplete(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.awaitAllSuccessfulOrThrow().onComplete(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.awaitAll().onComplete(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.allUntil(_ -> false).onComplete(subtask));
+
+            done.countDown();
             scope.join();
-            assertTrue(subtask.get());
+
+            // onFork with completed task should throw IAE
+            assertEquals(Subtask.State.SUCCESS, subtask.state());
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.allSuccessfulOrThrow().onFork(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.anySuccessfulResultOrThrow().onFork(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.awaitAllSuccessfulOrThrow().onFork(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.awaitAll().onFork(subtask));
+            assertThrows(IllegalArgumentException.class,
+                    () -> Joiner.allUntil(_ -> false).onFork(subtask));
+        }
+
+    }
+
+    /**
+     * Test the Config function apply method throwing an exception.
+     */
+    @Test
+    void testConfigFunctionThrows() throws Exception {
+        assertThrows(FooException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(),
+                                               cf -> { throw new FooException(); }));
+    }
+
+    /**
+     * Test Config equals/hashCode/toString
+     */
+    @Test
+    void testConfigMethods() throws Exception {
+        Function<Config, Config> testConfig = cf -> {
+            var name = "duke";
+            var threadFactory = Thread.ofPlatform().factory();
+            var timeout = Duration.ofSeconds(10);
+
+            assertEquals(cf, cf);
+            assertEquals(cf.withName(name), cf.withName(name));
+            assertEquals(cf.withThreadFactory(threadFactory), cf.withThreadFactory(threadFactory));
+            assertEquals(cf.withTimeout(timeout), cf.withTimeout(timeout));
+
+            assertNotEquals(cf, cf.withName(name));
+            assertNotEquals(cf, cf.withThreadFactory(threadFactory));
+            assertNotEquals(cf, cf.withTimeout(timeout));
+
+            assertEquals(cf.withName(name).hashCode(), cf.withName(name).hashCode());
+            assertEquals(cf.withThreadFactory(threadFactory).hashCode(),
+                    cf.withThreadFactory(threadFactory).hashCode());
+            assertEquals(cf.withTimeout(timeout).hashCode(), cf.withTimeout(timeout).hashCode());
+
+            assertTrue(cf.withName(name).toString().contains(name));
+            assertTrue(cf.withThreadFactory(threadFactory).toString().contains(threadFactory.toString()));
+            assertTrue(cf.withTimeout(timeout).toString().contains(timeout.toString()));
+
+            return cf;
+        };
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), testConfig)) {
+            // do nothing
         }
     }
 
     /**
      * Test for NullPointerException.
      */
     @Test
     void testNulls() throws Exception {
-        assertThrows(NullPointerException.class, () -> new StructuredTaskScope("", null));
-        try (var scope = new StructuredTaskScope<Object>()) {
-            assertThrows(NullPointerException.class, () -> scope.fork(null));
-            assertThrows(NullPointerException.class, () -> scope.joinUntil(null));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(null));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(null, cf -> cf));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), null));
+
+        assertThrows(NullPointerException.class, () -> Joiner.allUntil(null));
+
+        // fork
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            assertThrows(NullPointerException.class, () -> scope.fork((Callable<Object>) null));
+            assertThrows(NullPointerException.class, () -> scope.fork((Runnable) null));
+        }
+
+        // Config and withXXX methods
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> null));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName(null)));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withThreadFactory(null)));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withTimeout(null)));
+
+        // Joiner.onFork/onComplete
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAllSuccessfulOrThrow().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAllSuccessfulOrThrow().onComplete(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAll().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAll().onComplete(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.allSuccessfulOrThrow().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.allSuccessfulOrThrow().onComplete(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.anySuccessfulResultOrThrow().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.anySuccessfulResultOrThrow().onComplete(null));
+    }
+
+    /**
+     * ThreadFactory that counts usage.
+     */
+    private static class CountingThreadFactory implements ThreadFactory {
+        final ThreadFactory delegate;
+        final AtomicInteger threadCount = new AtomicInteger();
+        CountingThreadFactory(ThreadFactory delegate) {
+            this.delegate = delegate;
+        }
+        @Override
+        public Thread newThread(Runnable task) {
+            threadCount.incrementAndGet();
+            return delegate.newThread(task);
+        }
+        int threadCount() {
+            return threadCount.get();
         }
+    }
 
-        assertThrows(NullPointerException.class, () -> new ShutdownOnSuccess<Object>("", null));
-        try (var scope = new ShutdownOnSuccess<Object>()) {
-            assertThrows(NullPointerException.class, () -> scope.fork(null));
-            assertThrows(NullPointerException.class, () -> scope.joinUntil(null));
-            assertThrows(NullPointerException.class, () -> scope.result(null));
+    /**
+     * A joiner that counts that counts the number of subtasks that are forked and the
+     * number of subtasks that complete.
+     */
+    private static class CountingJoiner<T> implements Joiner<T, Void> {
+        final AtomicInteger onForkCount = new AtomicInteger();
+        final AtomicInteger onCompleteCount = new AtomicInteger();
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            onForkCount.incrementAndGet();
+            return false;
         }
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            onCompleteCount.incrementAndGet();
+            return false;
+        }
+        @Override
+        public Void result() {
+            return null;
+        }
+        int onForkCount() {
+            return onForkCount.get();
+        }
+        int onCompleteCount() {
+            return onCompleteCount.get();
+        }
+    }
 
-        assertThrows(NullPointerException.class, () -> new ShutdownOnFailure("", null));
-        try (var scope = new ShutdownOnFailure()) {
-            assertThrows(NullPointerException.class, () -> scope.fork(null));
-            assertThrows(NullPointerException.class, () -> scope.joinUntil(null));
-            assertThrows(NullPointerException.class, () -> scope.throwIfFailed(null));
+    /**
+     * A joiner that cancels execution when a subtask completes. It also keeps a count
+     * of the number of subtasks that are forked and the number of subtasks that complete.
+     */
+    private static class CancelAfterOneJoiner<T> implements Joiner<T, Void> {
+        final AtomicInteger onForkCount = new AtomicInteger();
+        final AtomicInteger onCompleteCount = new AtomicInteger();
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            onForkCount.incrementAndGet();
+            return false;
+        }
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            onCompleteCount.incrementAndGet();
+            return true;
+        }
+        @Override
+        public Void result() {
+            return null;
+        }
+        int onForkCount() {
+            return onForkCount.get();
+        }
+        int onCompleteCount() {
+            return onCompleteCount.get();
         }
     }
 
     /**
      * A runtime exception for tests.
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -29,10 +29,11 @@
  * @library /test/lib
  * @run junit/othervm StructuredThreadDumpTest
  */
 
 import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.concurrent.ThreadFactory;
@@ -51,12 +52,11 @@
      * Test that a thread dump with a tree of task scopes contains a thread grouping for
      * each task scope.
      */
     @Test
     void testTree() throws Exception {
-        ThreadFactory factory = Thread.ofVirtual().factory();
-        try (var scope = new StructuredTaskScope<>("scope", factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope"))) {
             Thread thread1 = fork(scope, "child-scope-A");
             Thread thread2 = fork(scope, "child-scope-B");
             try {
                 ThreadDump threadDump = threadDump();
 
@@ -81,11 +81,12 @@
                 // thread1 and threads2 should be in threads array of "scope"
                 container1.findThread(thread1.threadId()).orElseThrow();
                 container1.findThread(thread2.threadId()).orElseThrow();
 
             } finally {
-                scope.shutdown();
+                LockSupport.unpark(thread1);
+                LockSupport.unpark(thread2);
                 scope.join();
             }
         }
     }
 
@@ -93,15 +94,14 @@
      * Test that a thread dump with nested tasks scopes contains a thread grouping for
      * each task scope.
      */
     @Test
     void testNested() throws Exception {
-        ThreadFactory factory = Thread.ofVirtual().factory();
-        try (var scope1 = new StructuredTaskScope<>("scope-A", factory)) {
+        try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope-A"))) {
             Thread thread1 = fork(scope1);
 
-            try (var scope2 = new StructuredTaskScope<>("scope-B", factory)) {
+            try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope-B"))) {
                 Thread thread2 = fork(scope2);
                 try {
                     ThreadDump threadDump = threadDump();
 
                     // thread dump should have a thread container for both scopes
@@ -125,15 +125,15 @@
 
                     // thread2 should be in threads array of "scope-B"
                     container2.findThread(thread2.threadId()).orElseThrow();
 
                 } finally {
-                    scope2.shutdown();
+                    LockSupport.unpark(thread2);
                     scope2.join();
                 }
             } finally {
-                scope1.shutdown();
+                LockSupport.unpark(thread1);
                 scope1.join();
             }
         }
     }
 
@@ -158,11 +158,11 @@
 
     /**
      * Forks a subtask in the given scope that parks, returning the Thread that executes
      * the subtask.
      */
-    private static Thread fork(StructuredTaskScope<Object> scope) throws Exception {
+    private static Thread fork(StructuredTaskScope<Object, Void> scope) throws Exception {
         var ref = new AtomicReference<Thread>();
         scope.fork(() -> {
             ref.set(Thread.currentThread());
             LockSupport.park();
             return null;
@@ -176,20 +176,19 @@
 
     /**
      * Forks a subtask in the given scope. The subtask creates a new child scope with
      * the given name, then parks. This method returns Thread that executes the subtask.
      */
-    private static Thread fork(StructuredTaskScope<Object> scope,
+    private static Thread fork(StructuredTaskScope<Object, Void> scope,
                                String childScopeName) throws Exception {
         var ref = new AtomicReference<Thread>();
         scope.fork(() -> {
-            ThreadFactory factory = Thread.ofVirtual().factory();
-            try (var childScope = new StructuredTaskScope<Object>(childScopeName, factory)) {
+            try (var childScope = StructuredTaskScope.open(Joiner.awaitAll(),
+                    cf -> cf.withName(childScopeName))) {
                 ref.set(Thread.currentThread());
                 LockSupport.park();
             }
-            return null;
         });
         Thread thread;
         while ((thread = ref.get()) == null) {
             Thread.sleep(10);
         }
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java b/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java
@@ -29,10 +29,11 @@
  * @run junit WithScopedValue
  */
 
 import java.util.concurrent.StructuredTaskScope;
 import java.util.concurrent.StructuredTaskScope.Subtask;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.util.concurrent.StructureViolationException;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Stream;
 
@@ -53,11 +54,12 @@
     @ParameterizedTest
     @MethodSource("factories")
     void testForkInheritsScopedValue1(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
         String value = ScopedValue.where(name, "x").call(() -> {
-            try (var scope = new StructuredTaskScope<String>(null, factory)) {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                      cf -> cf.withThreadFactory(factory))) {
                 Subtask<String> subtask = scope.fork(() -> {
                     return name.get(); // child should read "x"
                 });
                 scope.join();
                 return subtask.get();
@@ -72,13 +74,15 @@
     @ParameterizedTest
     @MethodSource("factories")
     void testForkInheritsScopedValue2(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
         String value = ScopedValue.where(name, "x").call(() -> {
-            try (var scope1 = new StructuredTaskScope<String>(null, factory)) {
+            try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                       cf -> cf.withThreadFactory(factory))) {
                 Subtask<String> subtask1 = scope1.fork(() -> {
-                    try (var scope2 = new StructuredTaskScope<String>(null, factory)) {
+                    try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                               cf -> cf.withThreadFactory(factory))) {
                         Subtask<String> subtask2 = scope2.fork(() -> {
                             return name.get(); // grandchild should read "x"
                         });
                         scope2.join();
                         return subtask2.get();
@@ -97,17 +101,19 @@
     @ParameterizedTest
     @MethodSource("factories")
     void testForkInheritsScopedValue3(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
         String value = ScopedValue.where(name, "x").call(() -> {
-            try (var scope1 = new StructuredTaskScope<String>(null, factory)) {
+            try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                       cf -> cf.withThreadFactory(factory))) {
                 Subtask<String> subtask1 = scope1.fork(() -> {
                     assertEquals(name.get(), "x");  // child should read "x"
 
                     // rebind name to "y"
                     String grandchildValue = ScopedValue.where(name, "y").call(() -> {
-                        try (var scope2 = new StructuredTaskScope<String>(null, factory)) {
+                        try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                                   cf -> cf.withThreadFactory(factory))) {
                             Subtask<String> subtask2 = scope2.fork(() -> {
                                 return name.get(); // grandchild should read "y"
                             });
                             scope2.join();
                             return subtask2.get();
@@ -129,33 +135,33 @@
      */
     @Test
     void testStructureViolation1() throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
         class Box {
-            StructuredTaskScope<Object> scope;
+            StructuredTaskScope<Object, Void> scope;
         }
         var box = new Box();
         try {
             try {
                 ScopedValue.where(name, "x").run(() -> {
-                    box.scope = new StructuredTaskScope<Object>();
+                    box.scope = StructuredTaskScope.open(Joiner.awaitAll());
                 });
                 fail();
             } catch (StructureViolationException expected) { }
 
             // underlying flock should be closed and fork should fail to start a thread
-            StructuredTaskScope<Object> scope = box.scope;
+            StructuredTaskScope<Object, Void> scope = box.scope;
             AtomicBoolean ran = new AtomicBoolean();
             Subtask<Object> subtask = scope.fork(() -> {
                 ran.set(true);
                 return null;
             });
             scope.join();
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertFalse(ran.get());
         } finally {
-            StructuredTaskScope<Object> scope = box.scope;
+            StructuredTaskScope<Object, Void> scope = box.scope;
             if (scope != null) {
                 scope.close();
             }
         }
     }
@@ -164,12 +170,12 @@
      * Test closing a StructuredTaskScope while executing in a dynamic scope.
      */
     @Test
     void testStructureViolation2() throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        try (var scope = new StructuredTaskScope<String>()) {
-                ScopedValue.where(name, "x").run(() -> {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            ScopedValue.where(name, "x").run(() -> {
                 assertThrows(StructureViolationException.class, scope::close);
             });
         }
     }
 
@@ -177,12 +183,12 @@
      * Test fork when a scoped value is bound after a StructuredTaskScope is created.
      */
     @Test
     void testStructureViolation3() throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        try (var scope = new StructuredTaskScope<String>()) {
-                ScopedValue.where(name, "x").run(() -> {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            ScopedValue.where(name, "x").run(() -> {
                 assertThrows(StructureViolationException.class,
                         () -> scope.fork(() -> "foo"));
             });
         }
     }
@@ -195,22 +201,22 @@
         ScopedValue<String> name1 = ScopedValue.newInstance();
         ScopedValue<String> name2 = ScopedValue.newInstance();
 
         // rebind
         ScopedValue.where(name1, "x").run(() -> {
-            try (var scope = new StructuredTaskScope<String>()) {
-                    ScopedValue.where(name1, "y").run(() -> {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+                ScopedValue.where(name1, "y").run(() -> {
                     assertThrows(StructureViolationException.class,
                             () -> scope.fork(() -> "foo"));
                 });
             }
         });
 
         // new binding
         ScopedValue.where(name1, "x").run(() -> {
-            try (var scope = new StructuredTaskScope<String>()) {
-                    ScopedValue.where(name2, "y").run(() -> {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+                ScopedValue.where(name2, "y").run(() -> {
                     assertThrows(StructureViolationException.class,
                             () -> scope.fork(() -> "foo"));
                 });
             }
         });
diff a/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java b/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java
--- a/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java
+++ b/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java
@@ -63,11 +63,12 @@
             return null;
         });
 
         // Observable in structured concurrency in SV mode, but not in ACC mode
         Subject.callAs(subject, () -> {
-            try (var scope = new StructuredTaskScope<>()) {
+            var joiner = StructuredTaskScope.Joiner.awaitAll();
+            try (var scope = StructuredTaskScope.open(joiner)) {
                 scope.fork(() -> check(3, Subject.current(), usv ? "Duke" : null));
                 scope.join();
             }
             return null;
         });
diff a/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java b/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java
--- a/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java
+++ b/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java
@@ -748,55 +748,10 @@
                 latch.countDown();
             }
         }
     }
 
-    /**
-     * Test wakeup is flock confined.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testWakeupConfined(ThreadFactory factory) throws Exception {
-        try (var flock = ThreadFlock.open(null)) {
-            // thread in flock
-            testWakeupConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                return flock.start(thread);
-            });
-
-            // thread not in flock
-            testWakeupConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                thread.start();
-                return thread;
-            });
-        }
-    }
-
-    /**
-     * Test that a thread created with the given factory cannot wakeup the
-     * given flock.
-     */
-    private void testWakeupConfined(ThreadFlock flock,
-                                    Function<Runnable, Thread> factory) throws Exception {
-        var exception = new AtomicReference<Exception>();
-        Thread thread = factory.apply(() -> {
-            try {
-                flock.wakeup();
-            } catch (Exception e) {
-                exception.set(e);
-            }
-        });
-        thread.join();
-        Throwable cause = exception.get();
-        if (flock.containsThread(thread)) {
-            assertNull(cause);
-        } else {
-            assertTrue(cause instanceof WrongThreadException);
-        }
-    }
-
     /**
      * Test close with no threads running.
      */
     @Test
     void testCloseWithNoThreads() {
@@ -931,63 +886,10 @@
             assertTrue(Thread.interrupted());  // clear interrupt
         }
         assertNull(exception.get());
     }
 
-    /**
-     * Test shutdown is confined to threads in the flock.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownConfined(ThreadFactory factory) throws Exception {
-        try (var flock = ThreadFlock.open(null)) {
-            // thread in flock
-            testShutdownConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                return flock.start(thread);
-            });
-
-            // thread in flock
-            try (var flock2 = ThreadFlock.open(null)) {
-                testShutdownConfined(flock, task -> {
-                    Thread thread = factory.newThread(task);
-                    return flock2.start(thread);
-                });
-            }
-
-            // thread not contained in flock
-            testShutdownConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                thread.start();
-                return thread;
-            });
-        }
-    }
-
-    /**
-     * Test that a thread created with the given factory cannot shut down the
-     * given flock.
-     */
-    private void testShutdownConfined(ThreadFlock flock,
-                                      Function<Runnable, Thread> factory) throws Exception {
-        var exception = new AtomicReference<Exception>();
-        Thread thread = factory.apply(() -> {
-            try {
-                flock.shutdown();
-            } catch (Exception e) {
-                exception.set(e);
-            }
-        });
-        thread.join();
-        Throwable cause = exception.get();
-        if (flock.containsThread(thread)) {
-            assertNull(cause);
-        } else {
-            assertTrue(cause instanceof WrongThreadException);
-        }
-    }
-
     /**
      * Test that closing an enclosing thread flock closes a nested thread flocks.
      */
     @Test
     void testStructureViolation() {
diff a/test/langtools/ProblemList-Virtual.txt b/test/langtools/ProblemList-Virtual.txt
--- /dev/null
+++ b/test/langtools/ProblemList-Virtual.txt
@@ -0,0 +1,31 @@
+####
+## Filed bugs
+
+####
+# Incompatible tests.
+
+####
+# Failed because jtreg wrapper can't catch exceptions in thread groups
+# or incorrently process negative tests
+tools/javac/NullQualifiedNew.java 0000000 generic-all
+tools/javac/NullQualifiedSuper2.java 0000000 generic-all
+tools/javac/generics/T5094318.java 0000000 generic-all
+tools/javac/api/T6395981.java 0000000 generic-all
+jdk/javadoc/tool/sourceOption/SourceOption.java 0000000 generic-all
+
+####
+# Tests failed because of unexpected --enable-preview
+tools/javac/launcher/GetResourceTest.java 0000000 generic-all
+tools/javac/launcher/SourceLauncherTest.java 0000000 generic-all
+
+# unstable when main thread is a virtual thread
+jdk/jshell/ToolMultilineSnippetHistoryTest.java 0000000 generic-all
+jdk/jshell/ToolShiftTabTest.java 0000000 generic-all
+
+# more unstable when main thread is a virtual thread
+jdk/jshell/ToolTabCommandTest.java 8287078 generic-all
+jdk/jshell/ToolTabSnippetTest.java 8287078 generic-all
+jdk/jshell/UndefinedClassTest.java 8287078 generic-all
+
+# output not as expected
+jdk/javadoc/doclet/testLinkOption/TestRedirectLinks.java 8338439 generic-all
diff a/test/langtools/ProblemList-vthread-Xcomp.txt b/test/langtools/ProblemList-vthread-Xcomp.txt
--- /dev/null
+++ b/test/langtools/ProblemList-vthread-Xcomp.txt
@@ -0,0 +1,34 @@
+#
+# Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+
+#############################################################################
+#
+# List of quarantined tests for testing in Xcomp mode with wrapper.
+#
+#############################################################################
+
+####
+## Timeouts
+
+tools/javac/failover/CheckAttributedTree.java
+tools/sjavac/IdleShutdown.java
diff a/test/langtools/ProblemList.txt b/test/langtools/ProblemList.txt
--- a/test/langtools/ProblemList.txt
+++ b/test/langtools/ProblemList.txt
@@ -60,10 +60,12 @@
 tools/javac/annotations/typeAnnotations/referenceinfos/Lambda.java              8057687    generic-all    emit correct byte code an attributes for type annotations
 tools/javac/annotations/typeAnnotations/referenceinfos/NestedTypes.java         8057687    generic-all    emit correct byte code an attributes for type annotations
 tools/javac/warnings/suppress/TypeAnnotations.java                              8057683    generic-all    improve ordering of errors with type annotations
 tools/javac/modules/SourceInSymlinkTest.java                                    8180263    windows-all    fails when run on a subst drive
 
+tools/javac/lambda/T8031967.java                                                8323041    linux-aarch64  fails with -XX:TieredStopAtLevel=n (n=1,2,3)
+
 ###########################################################################
 #
 # javap
 
 tools/javap/output/RepeatingTypeAnnotations.java                                8057687    generic-all    emit correct byte code an attributes for type annotations
diff a/make/langtools/test/TEST.ROOT b/test/lib-test/ProblemList-Virtual.txt
--- a/make/langtools/test/TEST.ROOT
+++ b/test/lib-test/ProblemList-Virtual.txt
diff a/test/micro/org/openjdk/bench/loom/Monitors.java b/test/micro/org/openjdk/bench/loom/Monitors.java
--- /dev/null
+++ b/test/micro/org/openjdk/bench/loom/Monitors.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.openjdk.bench.loom;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import jdk.internal.vm.Continuation;
+import jdk.internal.vm.ContinuationScope;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.TimeUnit;
+
+//@Fork(2)
+@Fork(1)
+@Warmup(iterations = 3, time = 5)
+@Measurement(iterations = 3, time = 5)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@State(Scope.Thread)
+@SuppressWarnings("preview")
+public class Monitors {
+    static final ContinuationScope SCOPE = new ContinuationScope() { };
+
+    static class RunSynchronized implements Runnable {
+        private final int stackDepth;
+        private final Object[] syncLockArray;
+
+        private RunSynchronized(int stackDepth, ReentrantLock[] lockArray) {
+            this.stackDepth = stackDepth;
+            this.syncLockArray = lockArray;
+        }
+
+        public void recursive(int depth) {
+            if (syncLockArray[depth] != null) {
+                synchronized (syncLockArray[depth]) {
+                    if (depth > 0) {
+                        recursive(depth - 1);
+                    } else {
+                        Continuation.yield(SCOPE);
+                    }
+                }
+            } else {
+                if (depth > 0) {
+                    recursive(depth - 1);
+                } else {
+                    Continuation.yield(SCOPE);
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            while (true) {
+                recursive(stackDepth);
+            }
+        }
+    }
+
+    static class RunReentrantLock implements Runnable {
+        private final int stackDepth;
+        private final ReentrantLock[] lockArray;
+
+        private RunReentrantLock(int stackDepth, ReentrantLock[] lockArray) {
+            this.stackDepth = stackDepth;
+            this.lockArray = lockArray;
+        }
+
+        public void recursive(int depth) {
+            if (lockArray[depth] != null) {
+                lockArray[depth].lock();
+                if (depth > 0) {
+                    recursive(depth - 1);
+                } else {
+                    Continuation.yield(SCOPE);
+                }
+                lockArray[depth].unlock();
+            } else {
+                if (depth > 0) {
+                    recursive(depth - 1);
+                } else {
+                    Continuation.yield(SCOPE);
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            while (true) {
+                recursive(stackDepth);
+            }
+        }
+    }
+
+    static void inflateAllLocks(ReentrantLock[] lockArray) {
+        for (ReentrantLock lock : lockArray) {
+            if (lock != null) {
+                synchronized (lock) {
+                    try {
+                        lock.wait(1);
+                    } catch (Exception e) {}
+                }
+            }
+        }
+    }
+
+    static ReentrantLock[] createLockArray(int stackDepth, int monitorCount) {
+        ReentrantLock[] lockArray = new ReentrantLock[stackDepth + 1];
+
+        // Always a lock on the bottom most frame
+        lockArray[stackDepth] = new ReentrantLock();
+        // Limit extra monitors to no more than one per recursive frame.
+        int remainingMonitors = Math.min(Math.max(0, monitorCount - 1), stackDepth);
+
+        if (remainingMonitors > 0) {
+            // Calculate which other frames will use a lock.
+            int stride = Math.max(2, (stackDepth / monitorCount));
+            int frameIndex = (stackDepth - stride) % (stackDepth + 1);
+            while (remainingMonitors > 0) {
+                if (lockArray[frameIndex] == null) {
+                    lockArray[frameIndex] = new ReentrantLock();
+                    frameIndex = (frameIndex - stride) % (stackDepth + 1);
+                    remainingMonitors--;
+                } else {
+                    frameIndex = (frameIndex - 1) % (stackDepth + 1);
+                }
+            }
+        }
+        System.out.println("Created lock array. synchronized/ReentrantLock will be used at following indexes:");
+        for (int i = stackDepth; i >= 0; i--) {
+            System.out.println("[" + i + "] : " + (lockArray[i] != null ? "synchronized/ReentrantLock" : "-"));
+        }
+        return lockArray;
+    }
+
+    static Continuation createContinuation(int stackDepth, ReentrantLock[] lockArray, boolean useSyncronized) {
+        Runnable task = useSyncronized ? new RunSynchronized(stackDepth, lockArray)
+                                       : new RunReentrantLock(stackDepth, lockArray);
+        //inflateAllLocks(lockArray);
+        return new Continuation(SCOPE, task);
+    }
+
+    @State(Scope.Thread)
+    public static class MyState {
+        @Param({"0", "5", "10", "20"})
+        public int stackDepth;
+
+        @Param({"1", "2", "3"})
+        public int monitorCount;
+
+        public Continuation syncCont;
+        public Continuation reentCont;
+
+        @Setup(Level.Trial)
+        public void doSetup() {
+            ReentrantLock[] lockArray = createLockArray(stackDepth, monitorCount);
+            // We don't care which object we synchronize on so just pass
+            // the same ReentrantLock array to the synchronized task.
+            syncCont = createContinuation(stackDepth, lockArray, true /* useSyncronized */);
+            reentCont = createContinuation(stackDepth, lockArray, false /* useSyncronized */);
+            System.out.println("Created continuation used for testing with stackDepth: " + stackDepth + " and monitorCount: " + monitorCount);
+        }
+    }
+
+    @Benchmark
+    public void syncLock(MyState state) {
+        state.syncCont.run();
+    }
+
+    @Benchmark
+    public void reentrantLock(MyState state) {
+        state.reentCont.run();
+    }
+}
diff a/test/micro/org/openjdk/bench/loom/Monitors2.java b/test/micro/org/openjdk/bench/loom/Monitors2.java
--- /dev/null
+++ b/test/micro/org/openjdk/bench/loom/Monitors2.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.openjdk.bench.loom;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.*;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+//@Fork(2)
+@Fork(1)
+@Warmup(iterations = 3, time = 5)
+@Measurement(iterations = 3, time = 5)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@State(Scope.Thread)
+@SuppressWarnings("preview")
+public class Monitors2 {
+    static Object[] globalLockArray;
+    static ReentrantLock[] globalReentrantLockArray;
+    static AtomicInteger workerCount = new AtomicInteger(0);
+    static AtomicInteger dummyCounter = new AtomicInteger(0);
+    static int workIterations;
+
+    static int VT_COUNT;
+    static int CARRIER_COUNT = 8;
+    static ExecutorService scheduler = Executors.newFixedThreadPool(CARRIER_COUNT);
+
+    //@Param({"8", "32", "256", "4096", "131072"})
+    @Param({"8", "32", "256", "4096"})
+    static int VT_MULTIPLIER;
+
+    @Param({"1", "5", "12", "32"})
+    static int MONITORS_CNT;
+
+    @Param({"0", "50000", "100000", "200000"})
+    static int WORKLOAD;
+
+    @Param({"10"})
+    static int STACK_DEPTH;
+
+    void recursive4_1(int depth, int lockNumber) {
+        if (depth > 0) {
+            recursive4_1(depth - 1, lockNumber);
+        } else {
+            if (Math.random() < 0.5) {
+                Thread.yield();
+            }
+            recursive4_2(lockNumber);
+        }
+    }
+
+    void recursive4_2(int lockNumber) {
+        if (lockNumber + 2 <= MONITORS_CNT - 1) {
+            lockNumber += 2;
+            synchronized(globalLockArray[lockNumber]) {
+                Thread.yield();
+                recursive4_2(lockNumber);
+            }
+        }
+    }
+
+    final Runnable FOO4 = () -> {
+        int iterations = 10;
+        while (iterations-- > 0) {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT - 1);
+            synchronized(globalLockArray[lockNumber]) {
+                recursive4_1(lockNumber, lockNumber);
+            }
+        }
+        workerCount.getAndIncrement();
+    };
+
+    /**
+     *  Test contention on monitorenter with extra monitors on stack shared by all threads.
+     */
+    //@Benchmark
+    public void testContentionMultipleMonitors(MyState state) throws Exception {
+        workerCount.getAndSet(0);
+
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i] = virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO4);
+        }
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i].join();
+        }
+
+        if (workerCount.get() != VT_COUNT) {
+            throw new RuntimeException("testContentionMultipleMonitors failed. Expected " + VT_COUNT + "but found " + workerCount.get());
+        }
+    }
+
+    static void recursive5_1(int depth, int lockNumber, Object[] myLockArray) {
+        if (depth > 0) {
+            recursive5_1(depth - 1, lockNumber, myLockArray);
+        } else {
+            if (Math.random() < 0.5) {
+                Thread.yield();
+            }
+            recursive5_2(lockNumber, myLockArray);
+        }
+    }
+
+    static void recursive5_2(int lockNumber, Object[] myLockArray) {
+        if (lockNumber + 2 <= MONITORS_CNT - 1) {
+            lockNumber += 2;
+            synchronized (myLockArray[lockNumber]) {
+                if (Math.random() < 0.5) {
+                    Thread.yield();
+                }
+                synchronized (globalLockArray[lockNumber]) {
+                    Thread.yield();
+                    recursive5_2(lockNumber, myLockArray);
+                }
+            }
+        }
+    }
+
+    static final Runnable FOO5 = () -> {
+        Object[] myLockArray = new Object[MONITORS_CNT];
+        for (int i = 0; i < MONITORS_CNT; i++) {
+            myLockArray[i] = new Object();
+        }
+
+        int iterations = 10;
+        while (iterations-- > 0) {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT - 1);
+            synchronized (myLockArray[lockNumber]) {
+                synchronized (globalLockArray[lockNumber]) {
+                    recursive5_1(lockNumber, lockNumber, myLockArray);
+                }
+            }
+        }
+        workerCount.getAndIncrement();
+    };
+
+    /**
+     *  Test contention on monitorenter with extra monitors on stack both local only and shared by all threads.
+     */
+    //@Benchmark
+    public void testContentionMultipleMonitors2(MyState state) throws Exception {
+        workerCount.getAndSet(0);
+
+        // Create batch of VT threads.
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            //Thread.ofVirtual().name("FirstBatchVT-" + i).start(FOO);
+            batch[i] = virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO5);
+        }
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i].join();
+        }
+
+        if (workerCount.get() != VT_COUNT) {
+            throw new RuntimeException("testContentionMultipleMonitors2 failed. Expected " + VT_COUNT + "but found " + workerCount.get());
+        }
+    }
+
+    static void recursiveSync(int depth) {
+        if (depth > 0) {
+            recursiveSync(depth - 1);
+        } else {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT);
+            synchronized(globalLockArray[lockNumber]) {
+                //Thread.yield();
+                for (int i = 0; i < WORKLOAD; i++) {
+                    dummyCounter.getAndIncrement();
+                }
+                workerCount.getAndIncrement();
+            }
+        }
+    }
+
+    static final Runnable SYNC = () -> {
+        recursiveSync(STACK_DEPTH);
+    };
+
+    static void recursiveReentrant(int depth) {
+        if (depth > 0) {
+            recursiveReentrant(depth - 1);
+        } else {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT);
+            globalReentrantLockArray[lockNumber].lock();
+            //Thread.yield();
+            for (int i = 0; i < WORKLOAD; i++) {
+                dummyCounter.getAndIncrement();
+            }
+            workerCount.getAndIncrement();
+            globalReentrantLockArray[lockNumber].unlock();
+        }
+    }
+
+    static final Runnable REENTRANTLOCK = () -> {
+        recursiveReentrant(STACK_DEPTH);
+    };
+
+    public void runBenchmark(Runnable r) throws Exception {
+        workerCount.getAndSet(0);
+
+        // Create batch of VT threads.
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i] = virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(r);
+        }
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i].join();
+        }
+
+        if (workerCount.get() != VT_COUNT) {
+            throw new RuntimeException("testContentionMultipleMonitors2 failed. Expected " + VT_COUNT + "but found " + workerCount.get());
+        }
+    }
+
+    @Benchmark
+    public void testContentionReentrantLock(MyState state) throws Exception {
+        runBenchmark(REENTRANTLOCK);
+    }
+
+    @Benchmark
+    public void testContentionASync(MyState state) throws Exception {
+        runBenchmark(SYNC);
+    }
+
+    //@Benchmark
+    public void testExtraTime(MyState state) throws Exception {
+        dummyCounter.getAndSet(0);
+        // Takes ~120us
+        for (int i = 0; i < 50000; i++) {
+            dummyCounter.getAndIncrement();
+        }
+        if (dummyCounter.get() != 50000) {
+            throw new RuntimeException("testContentionMultipleMonitors2 failed. Expected " + 50000 + "but found " + dummyCounter.get());
+        }
+    }
+
+    @State(Scope.Thread)
+    public static class MyState {
+
+        @Setup(Level.Trial)
+        public void doSetup() {
+            // Setup up monitors/locks
+            globalLockArray = new Object[MONITORS_CNT];
+            globalReentrantLockArray = new ReentrantLock[MONITORS_CNT];
+            for (int i = 0; i < MONITORS_CNT; i++) {
+                globalLockArray[i] = new Object();
+                globalReentrantLockArray[i] = new ReentrantLock();
+            }
+            // Setup VirtualThread count
+            VT_COUNT = CARRIER_COUNT * VT_MULTIPLIER;
+
+            System.out.println("Running test with MONITORS_CNT = " + MONITORS_CNT + " VT_COUNT = " + VT_COUNT + " and WORKLOAD = " + WORKLOAD);
+            //System.out.println("Running test with VT_COUNT = " + VT_COUNT);
+        }
+
+        @TearDown(Level.Trial)
+        public void doTearDown() {
+            scheduler.shutdown();
+            System.out.println("Do TearDown");
+        }
+    }
+
+    private static Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler) {
+        Thread.Builder.OfVirtual builder = Thread.ofVirtual();
+        try {
+            Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder");
+            Constructor<?> ctor = clazz.getDeclaredConstructor(Executor.class);
+            ctor.setAccessible(true);
+            return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler);
+        } catch (InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof RuntimeException re) {
+                throw re;
+            }
+            throw new RuntimeException(e);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
