gatherers/: windowFixed, windowSliding, fold, scan, mapConcurrent, a custom Gatherer (dedupeConsecutive, takeUntil), and a Collector-vs-Gatherer comparison. Verified: windowSliding emits one short window on a too-short stream rather than shrinking to empty like windowFixed; mapConcurrent waits for in-flight siblings to finish (~901ms) rather than cancelling them when one mapper throws. scoped-values/: ScopedValue API and rebinding, why plain threads (virtual or platform) do not inherit a binding while a StructuredTaskScope subtask does, and the memory cost vs InheritableThreadLocal measured two ways -- a noisy heap-delta first pass, then a precise jcmd GC.class_histogram object census (5 shared Carrier objects vs ~700,000 copied ThreadLocalMap/Entry objects for 100,000 threads). Companion code for the ankurm.com posts "Java Stream Gatherers (JEP 485)" and "Scoped Values vs ThreadLocal in Java 25: Migration Guide with Virtual Threads". Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
75 lines
3.4 KiB
Java
75 lines
3.4 KiB
Java
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<String> 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";
|
|
}
|
|
}
|
|
}
|