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
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
$ java GathererBasics (JDK 25)
|
||||
windowFixed(3) of 1..7 = [[1, 2, 3], [4, 5, 6], [7]]
|
||||
CHECK ok : windowFixed(3) groups into [1,2,3][4,5,6][7], last window is the short remainder
|
||||
CHECK ok : windowFixed(0) throws IllegalArgumentException: 'windowSize' must be greater than zero
|
||||
windowSliding(3) of 1..5 = [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
|
||||
CHECK ok : windowSliding(3) of 5 elements produces exactly 3 overlapping windows, each length 3
|
||||
CHECK ok : windowSliding(3) on a 2-element stream still emits one short window: [[1, 2]]
|
||||
CHECK ok : windowSliding(3) on an empty stream emits nothing
|
||||
fold building a String: [1][2][3][4]
|
||||
CHECK ok : fold("", acc+"[n]") over 1..4 builds "[1][2][3][4]" left to right
|
||||
CHECK ok : fold's output stream has exactly one element, the final accumulation
|
||||
scan running total of 1..5 = [1, 3, 6, 10, 15]
|
||||
CHECK ok : scan(0, +) over 1..5 emits every prefix sum: [1,3,6,10,15]
|
||||
fold(0,+) over 1..3 = [6]
|
||||
scan(0,+) over 1..3 = [1, 3, 6]
|
||||
CHECK ok : fold(0,+) over 1..3 emits just the final sum [6]
|
||||
CHECK ok : scan(0,+) over 1..3 emits every partial sum [1,3,6]
|
||||
exit=0
|
||||
@@ -0,0 +1,9 @@
|
||||
$ java CollectorVsGatherer (JDK 25)
|
||||
Collectors.groupingBy totals = {amy=19.75, bo=23.99}
|
||||
CHECK ok : amy's total via groupingBy/summingDouble is 19.75
|
||||
CHECK ok : bo's total via groupingBy/summingDouble is 23.99
|
||||
scan -> filter -> map, one pipeline = [total=30, total=50, total=75]
|
||||
CHECK ok : scan feeding straight into filter/map in the same pipeline gives [total=30, total=50, total=75]
|
||||
windowFixed(2) over 7 elements, then collect(counting()) = 4
|
||||
CHECK ok : collect(counting()) after gather() proves the gather result is still a Stream you can collect further: 4 windows
|
||||
exit=0
|
||||
@@ -0,0 +1,7 @@
|
||||
$ java CustomGatherer (JDK 25)
|
||||
dedupeConsecutive of a,a,b,b,b,a,c,c = [a, b, a, c]
|
||||
CHECK ok : consecutive runs collapse to [a, b, a, c]; the second 'a' survives because it isn't adjacent to the first
|
||||
takeUntil(n == 3) result = [1, 2, 3], upstream elements actually pulled = [1, 2, 3]
|
||||
CHECK ok : takeUntil includes the element that satisfies the stop condition: [1, 2, 3]
|
||||
CHECK ok : the upstream peek() only saw 1, 2, 3 -- elements 4, 5, 6 were never pulled, this genuinely short-circuits
|
||||
exit=0
|
||||
@@ -0,0 +1,18 @@
|
||||
$ java MapConcurrentDemo (JDK 25)
|
||||
mapConcurrent(4, ...) results, completion order reversed: [10, 20, 30, 40]
|
||||
CHECK ok : mapConcurrent output preserves encounter order [10,20,30,40] even though task 1 finishes last
|
||||
mapConcurrent(2, ...) over 8 tasks, peak concurrent in-flight = 2
|
||||
CHECK ok : all 8 tasks completed
|
||||
CHECK ok : peak in-flight never exceeded maxConcurrency=2, measured peak=2
|
||||
CHECK ok : the cap is actually reached, not just respected -- measured peak=2
|
||||
VirtualThread[#38]/runnable@ForkJoinPool-1-worker-2 virtual=true
|
||||
VirtualThread[#39]/runnable@ForkJoinPool-1-worker-2 virtual=true
|
||||
VirtualThread[#40]/runnable@ForkJoinPool-1-worker-2 virtual=true
|
||||
VirtualThread[#41]/runnable@ForkJoinPool-1-worker-2 virtual=true
|
||||
VirtualThread[#42]/runnable@ForkJoinPool-1-worker-2 virtual=true
|
||||
VirtualThread[#37]/runnable@ForkJoinPool-1-worker-1 virtual=true
|
||||
CHECK ok : every mapConcurrent worker thread reports isVirtual() == true
|
||||
CHECK ok : 6 elements produced 6 distinct virtual threads -- one per element, not a shared pool
|
||||
mapConcurrent propagated: boom at element 3 after 901ms
|
||||
CHECK ok : the failure only surfaced after 901ms -- mapConcurrent waited for the other 4 in-flight tasks, it did not cancel them
|
||||
exit=0
|
||||
@@ -0,0 +1,15 @@
|
||||
$ java ScopedValuesBasics (JDK 25, no preview flag -- JEP 506 is final)
|
||||
CHECK ok : isBound() is true inside where().run()
|
||||
CHECK ok : get() returns "ankur" inside the binding
|
||||
CHECK ok : a method several frames deep, given no parameter, still sees the binding
|
||||
CHECK ok : isBound() is false again once run() has returned
|
||||
CHECK ok : get() outside a binding throws NoSuchElementException, not null or a default value
|
||||
CHECK ok : orElse(fallback) returns the fallback when unbound, no exception
|
||||
CHECK ok : orElse(fallback) returns the real value when bound
|
||||
CHECK ok : orElseThrow(supplier) throws exactly the supplied exception when unbound
|
||||
CHECK ok : outer binding: ankur
|
||||
CHECK ok : inner binding shadows the outer one: support-bot
|
||||
CHECK ok : a method called from inside the inner scope sees the REBOUND value, not the original
|
||||
CHECK ok : outer binding is restored, unchanged, once the inner where() block exits: ankur
|
||||
CHECK ok : call(...) returns whatever the lambda returns -- USER.get().length() == 5
|
||||
exit=0
|
||||
@@ -0,0 +1,14 @@
|
||||
$ jcmd <pid> GC.class_histogram (100000 subtasks parked, mode=sv, JDK 25)
|
||||
1: 100000 115102880 jdk.internal.vm.StackChunk ([email protected])
|
||||
2: 100000 16800000 java.lang.VirtualThread ([email protected])
|
||||
9: 100000 2400000 java.util.concurrent.StructuredTaskScopeImpl$SubtaskImpl ([email protected])
|
||||
128: 5 160 java.lang.ScopedValue$Carrier ([email protected])
|
||||
175: 5 80 java.lang.InheritableThreadLocal ([email protected])
|
||||
177: 5 80 java.lang.ScopedValue ([email protected])
|
||||
203: 2 48 java.lang.ScopedValue$Snapshot ([email protected])
|
||||
257: 1 32 java.util.concurrent.ThreadLocalRandom ([email protected])
|
||||
281: 1 24 java.util.concurrent.StructuredTaskScopeImpl$SubtaskImpl$AltResult ([email protected])
|
||||
292: 1 24 jdk.internal.vm.ScopedValueContainer$BindingsSnapshot ([email protected])
|
||||
324: 1 16 java.lang.ThreadLocal ([email protected])
|
||||
353: 1 16 java.util.concurrent.ThreadLocalRandom$Access$1 ([email protected])
|
||||
Total 1133788 172079104
|
||||
@@ -0,0 +1,15 @@
|
||||
$ jcmd <pid> GC.class_histogram (100000 subtasks parked, mode=tl, JDK 25)
|
||||
1: 100000 109386560 jdk.internal.vm.StackChunk ([email protected])
|
||||
2: 100000 16800000 java.lang.VirtualThread ([email protected])
|
||||
3: 500005 16000160 java.lang.ThreadLocal$ThreadLocalMap$Entry ([email protected])
|
||||
4: 100001 8000080 [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry; ([email protected])
|
||||
9: 100001 2400024 java.lang.ThreadLocal$ThreadLocalMap ([email protected])
|
||||
12: 100000 2400000 java.util.concurrent.StructuredTaskScopeImpl$SubtaskImpl ([email protected])
|
||||
177: 5 80 java.lang.InheritableThreadLocal ([email protected])
|
||||
179: 5 80 java.lang.ScopedValue ([email protected])
|
||||
258: 1 32 java.util.concurrent.ThreadLocalRandom ([email protected])
|
||||
281: 1 24 java.util.concurrent.StructuredTaskScopeImpl$SubtaskImpl$AltResult ([email protected])
|
||||
292: 1 24 jdk.internal.vm.ScopedValueContainer$BindingsSnapshot ([email protected])
|
||||
324: 1 16 java.lang.ThreadLocal ([email protected])
|
||||
353: 1 16 java.util.concurrent.ThreadLocalRandom$Access$1 ([email protected])
|
||||
Total 1770749 179468208
|
||||
@@ -0,0 +1,8 @@
|
||||
$ java --enable-preview InheritanceRequiresStructuredTaskScope (JDK 25)
|
||||
Thread.ofVirtual().start(...) from inside the bound block: UNBOUND
|
||||
CHECK ok : a plain virtual thread started from inside where().run() does NOT see the binding -- REQUEST_ID.get() threw NoSuchElementException
|
||||
new Thread(...) from inside the bound block: UNBOUND
|
||||
CHECK ok : a plain platform Thread doesn't see the binding either -- this isn't a virtual-thread-specific rule
|
||||
StructuredTaskScope.fork(...) from inside the bound block: req-42
|
||||
CHECK ok : a subtask forked from a StructuredTaskScope opened while REQUEST_ID is bound DOES see "req-42"
|
||||
exit=0
|
||||
@@ -0,0 +1,10 @@
|
||||
$ java --enable-preview -Xmx6g ScopedValueVsThreadLocalMemory 500000 (JDK 25)
|
||||
threads per scenario: 500000, 5 bound values each, forked via StructuredTaskScope
|
||||
CHECK ok : every one of 1000 subtasks read all 5 scoped values correctly
|
||||
CHECK ok : every one of 1000 subtasks read all 5 inherited ThreadLocal values correctly
|
||||
CHECK ok : every one of 500000 subtasks read all 5 scoped values correctly
|
||||
ScopedValue: 500000 live subtasks, 5 bound values each, heap delta = 531.8 MB (1115 bytes/thread)
|
||||
CHECK ok : every one of 500000 subtasks read all 5 inherited ThreadLocal values correctly
|
||||
ThreadLocal: 500000 live subtasks, 5 inherited values each, heap delta = 546.1 MB (1145 bytes/thread)
|
||||
ThreadLocal used 1.0x the heap ScopedValue used, for holding the identical 5 values live on 500000 threads
|
||||
exit=0
|
||||
Reference in New Issue
Block a user