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
82 lines
3.5 KiB
Java
82 lines
3.5 KiB
Java
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.StructuredTaskScope;
|
|
|
|
/**
|
|
* Parks N subtasks holding 5 bound values (mode "sv" = ScopedValue, mode "tl" =
|
|
* InheritableThreadLocal) and prints READY, then sleeps for 60s so an external `jcmd <pid>
|
|
* GC.class_histogram` can be run against it while every subtask is alive and holding its
|
|
* bindings. scripts/scoped-values.sh drives this and diffs the two histograms -- see
|
|
* docs/output/99-histogram-scopedvalue.txt and 99-histogram-threadlocal.txt.
|
|
*
|
|
* ScopedValueVsThreadLocalMemory.java measures the SAME comparison a coarser way (total heap
|
|
* delta before/after) and is honest about that measurement being noisy relative to the fixed cost
|
|
* of a VirtualThread + its StackChunk. This file exists because the coarse measurement alone
|
|
* would undersell a real, structural difference that a precise object count makes obvious.
|
|
* Chapter: docs/17-scoped-values-migration.md
|
|
*/
|
|
public class MemoryFootprintProbe {
|
|
|
|
static final ScopedValue<String> SV1 = ScopedValue.newInstance();
|
|
static final ScopedValue<String> SV2 = ScopedValue.newInstance();
|
|
static final ScopedValue<String> SV3 = ScopedValue.newInstance();
|
|
static final ScopedValue<String> SV4 = ScopedValue.newInstance();
|
|
static final ScopedValue<String> SV5 = ScopedValue.newInstance();
|
|
|
|
static final ThreadLocal<String> TL1 = new InheritableThreadLocal<>();
|
|
static final ThreadLocal<String> TL2 = new InheritableThreadLocal<>();
|
|
static final ThreadLocal<String> TL3 = new InheritableThreadLocal<>();
|
|
static final ThreadLocal<String> TL4 = new InheritableThreadLocal<>();
|
|
static final ThreadLocal<String> TL5 = new InheritableThreadLocal<>();
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
String mode = args[0];
|
|
int n = Integer.parseInt(args[1]);
|
|
CountDownLatch ready = new CountDownLatch(n);
|
|
CountDownLatch release = new CountDownLatch(1);
|
|
System.out.println("PID=" + ProcessHandle.current().pid() + " mode=" + mode + " n=" + n);
|
|
|
|
if (mode.equals("sv")) {
|
|
ScopedValue.where(SV1, "a").where(SV2, "b").where(SV3, "c").where(SV4, "d").where(SV5, "e").run(() -> {
|
|
try (var scope = StructuredTaskScope.open()) {
|
|
for (int i = 0; i < n; i++) {
|
|
scope.fork(() -> {
|
|
SV1.get();
|
|
ready.countDown();
|
|
release.await();
|
|
return null;
|
|
});
|
|
}
|
|
ready.await();
|
|
System.out.println("READY sv");
|
|
Thread.sleep(60_000);
|
|
release.countDown();
|
|
scope.join();
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
});
|
|
} else {
|
|
TL1.set("a");
|
|
TL2.set("b");
|
|
TL3.set("c");
|
|
TL4.set("d");
|
|
TL5.set("e");
|
|
try (var scope = StructuredTaskScope.open()) {
|
|
for (int i = 0; i < n; i++) {
|
|
scope.fork(() -> {
|
|
TL1.get();
|
|
ready.countDown();
|
|
release.await();
|
|
return null;
|
|
});
|
|
}
|
|
ready.await();
|
|
System.out.println("READY tl");
|
|
Thread.sleep(60_000);
|
|
release.countDown();
|
|
scope.join();
|
|
}
|
|
}
|
|
}
|
|
}
|