Files
javademos/scoped-values/src/MemoryFootprintProbe.java
T
Claude 6b5a918278 Add two standalone modules: Stream Gatherers (JEP 485) and Scoped Values vs ThreadLocal (JEP 506)
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
2026-09-24 04:27:12 +00:00

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();
}
}
}
}