import java.util.concurrent.StructuredTaskScope; /** * The single easiest wrong assumption about Scoped Values: that binding one makes it visible to * ANY child thread, the way InheritableThreadLocal does. It does not. A binding is only visible * to code running on the SAME thread, or to subtasks forked from a {@code StructuredTaskScope} * opened while the binding is active. A plain {@code Thread.ofVirtual().start(...)} or * {@code new Thread(...)} -- even started from inside the bound block -- sees an UNBOUND value. * * Compile and run with {@code --enable-preview --release 25}: StructuredTaskScope (JEP 505) is * still a preview API on JDK 25 (its sixth preview; finalized later, per docs/12-lanes-25-to-29.md * and docs/output/82-structured-not-final.txt in this repo) even though ScopedValue (JEP 506) * sitting right next to it is fully final. Mixing a final API with a preview one in the same * example is exactly the kind of thing that's easy to get wrong by skimming release notes. * * Chapter: docs/17-scoped-values-migration.md */ public class InheritanceRequiresStructuredTaskScope { static final ScopedValue REQUEST_ID = ScopedValue.newInstance(); public static void main(String[] args) throws Exception { ScopedValue.where(REQUEST_ID, "req-42").run(() -> { try { plainVirtualThreadDoesNotInherit(); plainPlatformThreadDoesNotInherit(); structuredTaskScopeForkDoesInherit(); } catch (Exception e) { throw new RuntimeException(e); } }); } static void plainVirtualThreadDoesNotInherit() throws InterruptedException { String[] outcome = new String[1]; Thread t = Thread.ofVirtual().start(() -> outcome[0] = readOrReport()); t.join(); System.out.println("Thread.ofVirtual().start(...) from inside the bound block: " + outcome[0]); Check.that(outcome[0].equals("UNBOUND"), "a plain virtual thread started from inside where().run() does NOT see the binding -- REQUEST_ID.get() threw NoSuchElementException"); } static void plainPlatformThreadDoesNotInherit() throws InterruptedException { String[] outcome = new String[1]; Thread t = new Thread(() -> outcome[0] = readOrReport()); t.start(); t.join(); System.out.println("new Thread(...) from inside the bound block: " + outcome[0]); Check.that(outcome[0].equals("UNBOUND"), "a plain platform Thread doesn't see the binding either -- this isn't a virtual-thread-specific rule"); } @SuppressWarnings("preview") static void structuredTaskScopeForkDoesInherit() throws Exception { String[] outcome = new String[1]; try (var scope = StructuredTaskScope.open()) { scope.fork(() -> { outcome[0] = readOrReport(); return null; }); scope.join(); } System.out.println("StructuredTaskScope.fork(...) from inside the bound block: " + outcome[0]); Check.that(outcome[0].equals("req-42"), "a subtask forked from a StructuredTaskScope opened while REQUEST_ID is bound DOES see \"req-42\""); } static String readOrReport() { try { return REQUEST_ID.get(); } catch (java.util.NoSuchElementException e) { return "UNBOUND"; } } }