Add virtual-threads-benchmark: re-run Spring Boot 4.1 / JDK 25 benchmarks, JEP 491 pinning fixed
Companion module for the rewritten post 'Virtual Threads on Spring Boot 4.1: The Benchmarks, Re-Run, and the Pinning Advice That Expired', retitled and re-benchmarked on Boot 4.1.1 / JDK 25.0.4.1 (the original post was written against Boot 3.4 / JDK 21). Covers: I/O-bound and CPU-bound throughput (platform vs virtual threads, including a JIT-warmup benchmarking bug this build caught and fixed), JEP 491 proof that synchronized no longer pins a virtual thread's carrier across a blocking call as of JDK 24 (obsoleting the old avoid-synchronized advice), proof that -Djdk.tracePinnedThreads=full is inert on JDK 25, and JEP 506's finalized ScopedValue API (JDK 25 GA, no --enable-preview, and a different shape than the old preview API). Kept as its own module rather than a new top-level repository, alongside the existing async/ module, which already has a stronger dual-JDK JEP 491 proof that this module's docs cross-link to instead of duplicating.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# 1. Enabling virtual threads on Spring Boot 4.1
|
||||
|
||||
[README](../README.md) | Next: [02-benchmark-methodology.md](02-benchmark-methodology.md)
|
||||
|
||||
Source: [`application.yml`](../src/main/resources/application.yml), [`DemoController.java`](../src/main/java/com/ankurm/vthreads/DemoController.java).
|
||||
Test: [`ThreadTypeTest.java`](../src/test/java/com/ankurm/vthreads/ThreadTypeTest.java).
|
||||
Transcript: [`docs/output/00-thread-type-confirmation.txt`](output/00-thread-type-confirmation.txt).
|
||||
|
||||
## Still one property
|
||||
|
||||
`spring.threads.virtual.enabled=true` is unchanged since Boot 3.2 and still the whole
|
||||
configuration surface for most apps on Boot 4.1. It requires JDK 21+; this repo runs it on
|
||||
JDK 25.
|
||||
|
||||
## What actually flips
|
||||
|
||||
Confirmed against a real running embedded server, not by reading the reference docs:
|
||||
|
||||
```
|
||||
enabled=false -> Thread: Thread[#9711,http-nio-auto-5-exec-1,5,main] | Virtual: false
|
||||
enabled=true -> Thread: VirtualThread[#9737,tomcat-handler-0]/runnable@ForkJoinPool-1-worker-2 | Virtual: true
|
||||
```
|
||||
|
||||
The platform-thread name prefix changed between the Boot 3 era and this repo's Boot 4.1 run
|
||||
(`http-nio-auto-5-exec-1` here vs. the `http-nio-8080-exec-3` shape in the older post) because
|
||||
this test binds to `server.port=0` (a random port) rather than a fixed 8080 -- Tomcat's
|
||||
connector-name-derived thread prefix reflects that. Cosmetic, but worth not being surprised by
|
||||
if your own logs look different from either example.
|
||||
|
||||
## Test infrastructure trap worth knowing about
|
||||
|
||||
Setting the property via `SpringApplicationBuilder.properties(...)` and expecting it to win
|
||||
over `application.yml` is a real bug this repo hit while building the benchmark below:
|
||||
`.properties(...)` binds to Spring's **`defaultProperties`** source, which has the *lowest*
|
||||
precedence of any property source Spring Boot recognizes -- lower than `application.yml`,
|
||||
which had `spring.threads.virtual.enabled: false` as its own explicit default. The fix was
|
||||
passing it as a command-line-style argument to `run(...)` instead
|
||||
(`"--spring.threads.virtual.enabled=" + value`), which binds at the highest precedence. See
|
||||
[`LoadBenchmarkTest.java`](../src/test/java/com/ankurm/vthreads/LoadBenchmarkTest.java) for
|
||||
where this was caught -- the first draft of the CPU-bound benchmark below silently ran both
|
||||
scenarios on platform threads and reported a difference that didn't exist.
|
||||
|
||||
Next: [02-benchmark-methodology.md](02-benchmark-methodology.md).
|
||||
@@ -0,0 +1,81 @@
|
||||
# 2. Benchmark methodology and results
|
||||
|
||||
[Previous: 01-enabling-virtual-threads.md](01-enabling-virtual-threads.md) | [README](../README.md) | Next: [03-pinning-diagnosis.md](03-pinning-diagnosis.md)
|
||||
|
||||
Source: [`DemoController.java`](../src/main/java/com/ankurm/vthreads/DemoController.java).
|
||||
Test: [`LoadBenchmarkTest.java`](../src/test/java/com/ankurm/vthreads/LoadBenchmarkTest.java).
|
||||
Transcripts: [`docs/output/01-io-bound-benchmark.txt`](output/01-io-bound-benchmark.txt), [`docs/output/02-cpu-bound-benchmark.txt`](output/02-cpu-bound-benchmark.txt).
|
||||
|
||||
## Why this repo doesn't reuse the old post's numbers
|
||||
|
||||
The version of this post it replaces benchmarked Boot 3.4.0 / JDK 21.0.3 with `wrk` (1,000
|
||||
connections, 30s) on a 4-core/8GB box. Neither `wrk` nor an equivalent load generator is
|
||||
available in this repo's build environment, and the environment itself is a 2 vCPU sandbox,
|
||||
not a 4-core server. Rather than copy the old numbers forward with a changed version number
|
||||
next to them, this repo builds its own concurrent load generator (`java.net.http.HttpClient`
|
||||
backed by a virtual-thread executor, itself only used as the *client* -- see
|
||||
`fireConcurrent()`) and reports what actually happened on this run, on this hardware, honestly
|
||||
labeled as such.
|
||||
|
||||
## I/O-bound: `/io`, `Thread.sleep(300)`
|
||||
|
||||
600 concurrent requests, each a 300ms simulated downstream call:
|
||||
|
||||
```
|
||||
platform threads : total=600 success=600 wall=1256ms p50=762ms p99=1193ms
|
||||
virtual threads : total=600 success=600 wall=675ms p50=531ms p99=654ms
|
||||
```
|
||||
|
||||
600 concurrent requests against Tomcat's default 200-thread platform pool queue in roughly
|
||||
three sequential batches of 200; each batch pays the full 300ms, so total wall time lands
|
||||
around 3 x 300ms plus scheduling overhead -- which is what the platform-thread row shows.
|
||||
Virtual threads create one thread per request and unmount for the duration of the sleep, so
|
||||
wall time stays close to a single 300ms round, regardless of how far past 200 the concurrency
|
||||
goes. The shape matches the original 2025 benchmark; the absolute numbers are this sandbox's,
|
||||
not that server's.
|
||||
|
||||
## CPU-bound: `/cpu`, 20,000x SHA-256 per request
|
||||
|
||||
This is the scenario worth reading carefully, because the first version of this benchmark
|
||||
was wrong, and the fix is itself worth knowing about.
|
||||
|
||||
**First cut, concurrency=16, no warm-up round:** virtual threads finished 4x faster than
|
||||
platform threads. That result doesn't make physical sense -- a CPU-bound virtual thread never
|
||||
unmounts, so it competes for the same physical cores a platform thread would, and should show
|
||||
no structural advantage. The cause: `MessageDigest.digest` and the servlet dispatch path get
|
||||
JIT-compiled **per class**, not per Spring context, and this benchmark starts a fresh
|
||||
`ApplicationContext` for each of the two scenarios inside the same JVM. Platform threads always
|
||||
ran first; by the time the virtual-thread scenario ran, the hot loop was already JIT-warmed --
|
||||
a benchmarking artifact with nothing to do with thread model, caught by the numbers being
|
||||
implausible rather than by inspecting the code.
|
||||
|
||||
**Fix:** an untimed warm-up round (`fireConcurrent` at a smaller concurrency, discarded)
|
||||
through the same code path before every timed measurement, in both scenarios.
|
||||
|
||||
**After the fix, concurrency=60** (chosen deliberately above this box's 2 vCPUs, so both
|
||||
thread models are forced into real core-bound saturation rather than fitting comfortably
|
||||
alongside each other):
|
||||
|
||||
```
|
||||
platform threads : total=60 success=60 wall=166ms p50=146ms p99=162ms
|
||||
virtual threads : total=60 success=60 wall=191ms p50=181ms p99=189ms
|
||||
```
|
||||
|
||||
Close, with platform threads and virtual threads trading the lead by a few percent across
|
||||
repeated runs (see the test's own run-to-run notes in
|
||||
[`LoadBenchmarkTest.java`](../src/test/java/com/ankurm/vthreads/LoadBenchmarkTest.java)) --
|
||||
consistent with the "no meaningful difference for CPU-bound work" claim every virtual threads
|
||||
article makes, but arrived at here by running it, catching a real measurement bug, fixing the
|
||||
methodology, and re-running it, rather than by assuming the claim was self-evidently true.
|
||||
|
||||
<blockquote>If your own CPU-bound benchmark shows virtual threads winning decisively, check
|
||||
your warm-up before you check your thread model. JIT compilation state leaking between two
|
||||
scenarios measured in the same JVM run is an easy way to manufacture a result that isn't
|
||||
real.</blockquote>
|
||||
|
||||
- If you're benchmarking your own service: match concurrency to something past your real
|
||||
platform-thread pool size for the I/O case, and past your core count for the CPU case --
|
||||
otherwise neither test forces the behaviour you're trying to observe.
|
||||
- JEP 444 (virtual threads, JDK 21): <https://openjdk.org/jeps/444> (`rel=nofollow`)
|
||||
|
||||
Next: [03-pinning-diagnosis.md](03-pinning-diagnosis.md).
|
||||
@@ -0,0 +1,104 @@
|
||||
# 3. Pinning diagnosis, corrected for JEP 491
|
||||
|
||||
[Previous: 02-benchmark-methodology.md](02-benchmark-methodology.md) | [README](../README.md) | Next: [04-scoped-value-and-checklist.md](04-scoped-value-and-checklist.md)
|
||||
|
||||
Source: [`PinningDemoService.java`](../src/main/java/com/ankurm/vthreads/PinningDemoService.java), [`PinningTraceCheckMain.java`](../src/main/java/com/ankurm/vthreads/PinningTraceCheckMain.java).
|
||||
Test: [`PinningJep491Test.java`](../src/test/java/com/ankurm/vthreads/PinningJep491Test.java).
|
||||
Script: [`scripts/check-trace-pinned-threads-removed.sh`](../scripts/check-trace-pinned-threads-removed.sh).
|
||||
Transcripts: [`docs/output/03a-pinning-jep491-proof.txt`](output/03a-pinning-jep491-proof.txt), [`docs/output/03b-trace-pinned-threads-removed.txt`](output/03b-trace-pinned-threads-removed.txt).
|
||||
|
||||
## The thing every virtual threads article says, that stopped being true in JDK 24
|
||||
|
||||
Every pre-JDK-24 article about virtual threads -- including the version of this post it
|
||||
replaces -- has some version of: "a virtual thread that enters a `synchronized` block and then
|
||||
blocks inside it cannot unmount; it pins its carrier thread for the whole operation." That was
|
||||
true through JDK 23. [JEP 491, "Synchronize Virtual Threads without Pinning"](https://openjdk.org/jeps/491)
|
||||
(`rel=nofollow`) shipped GA in **JDK 24** and changed it: as of JDK 24, ordinary `synchronized`
|
||||
blocks and methods, and `Object.wait()`, no longer pin. This repo runs JDK 25, so it inherits
|
||||
that behaviour, and the old advice is now wrong for anyone on JDK 24+.
|
||||
|
||||
A stronger version of this same proof already exists in a sibling ankurm.com companion repo:
|
||||
[`spring-async-demo/async`](https://ankurm.com/git.app/asmhatre/spring-async-demo/src/branch/main/async/docs/08-virtual-threads-and-pinning.md),
|
||||
for the article [@Async in Spring Boot 4: Executors, Virtual Threads and the Self-Invocation Trap](https://ankurm.com/spring-boot-4-async-executors-virtual-threads/),
|
||||
runs the **identical class file on two different JDKs** -- 21.0.12.1 and 25.0.4.1 -- and shows
|
||||
the same guarded run going from 4806ms (matching the pinned prediction almost exactly) to
|
||||
301ms. This repo only had JDK 25 available to test against, so its own proof below is a
|
||||
single-JDK version of the same result; if you want the direct before/after on two real JDK
|
||||
installs, that's the one to read.
|
||||
|
||||
## Proving it rather than citing it
|
||||
|
||||
The JEP text is the primary source, but the house standard here is a real run, not a citation.
|
||||
`PinningJep491Test` starts 60 virtual threads, each entering `synchronized` on its own,
|
||||
**distinct** lock object (so no thread ever contends with another for the lock itself -- any
|
||||
serialization observed is carrier-thread pinning, not ordinary lock contention), then sleeping
|
||||
250ms while still holding it:
|
||||
|
||||
```
|
||||
availableProcessors (default virtual-thread carrier pool size) = 2
|
||||
virtual threads = 60, each holds a DISTINCT monitor for 250ms
|
||||
predicted wall time IF PINNED (pre-JDK-24 behaviour): ~7500ms
|
||||
predicted wall time IF NOT PINNED (JDK 24+ behaviour): ~250ms, independent of carrier count
|
||||
|
||||
actual wall time: 253ms
|
||||
verdict: NOT PINNED -- matches JEP 491's documented JDK 24+ behaviour
|
||||
```
|
||||
|
||||
If this ran on JDK 23 or earlier, 60 virtual threads sharing a 2-thread carrier pool while each
|
||||
pins for 250ms would serialize into 30 sequential batches: roughly 7.5 seconds. It took 253ms.
|
||||
That's not "a bit better" -- it's the entire pinning cost gone for this pattern.
|
||||
|
||||
## The diagnostic flag from every one of those articles no longer does anything
|
||||
|
||||
`-Djdk.tracePinnedThreads=full` was the standard way to find pinning: pre-JDK-24, the JVM
|
||||
printed a stack trace to stdout every time a virtual thread pinned. JEP 491 removed the
|
||||
property along with most of the pinning it used to report on. `scripts/check-trace-pinned-threads-removed.sh`
|
||||
runs the same synchronized-then-sleep scenario twice, once with the flag and once without, and
|
||||
diffs the output:
|
||||
|
||||
```
|
||||
-- without -Djdk.tracePinnedThreads=full --
|
||||
java.version=25.0.4.1
|
||||
jdk.tracePinnedThreads=null
|
||||
Running a virtual thread that holds a monitor across Thread.sleep(200)...
|
||||
Done. If jdk.tracePinnedThreads still worked on this JDK, a pinned-thread stack trace would have printed above while the virtual thread was inside doWorkHoldingMonitor.
|
||||
|
||||
-- with -Djdk.tracePinnedThreads=full --
|
||||
java.version=25.0.4.1
|
||||
jdk.tracePinnedThreads=full
|
||||
Running a virtual thread that holds a monitor across Thread.sleep(200)...
|
||||
Done. If jdk.tracePinnedThreads still worked on this JDK, a pinned-thread stack trace would have printed above while the virtual thread was inside doWorkHoldingMonitor.
|
||||
|
||||
RESULT: no pinned-thread stack trace was printed by either run -- the flag is inert on this JDK, consistent with JEP 491
|
||||
```
|
||||
|
||||
Setting the property has no effect at all on JDK 25 -- not an error, not a deprecation
|
||||
warning, just silence either way. A team that kept `-Djdk.tracePinnedThreads=full` in their
|
||||
JVM flags through a JDK upgrade would get zero signal from it going forward and might not
|
||||
notice.
|
||||
|
||||
## What replaces it
|
||||
|
||||
JEP 491 kept the `jdk.VirtualThreadPinned` JFR event for the pinning that's still possible --
|
||||
it now reports both the pinning reason and the carrier thread's identity. The remaining case,
|
||||
[per the JEP itself](https://openjdk.org/jeps/491): a virtual thread that calls native code
|
||||
(a native method, or the Foreign Function & Memory API) which itself calls back into Java code
|
||||
that blocks or synchronizes. That's a narrower trigger than "any `synchronized` plus I/O" and
|
||||
one this repo does not attempt to reproduce -- constructing a real native-callback pinning
|
||||
case needs JNI, which is out of scope for a Spring Boot demo. Treat it as: still real, still
|
||||
worth the JFR event, much rarer in ordinary application code than the blanket old advice
|
||||
implied.
|
||||
|
||||
<blockquote><strong>If you're diagnosing a suspected pinning problem on JDK 24+,</strong>
|
||||
delete <code>-Djdk.tracePinnedThreads=full</code> from your flags -- it does nothing -- and
|
||||
capture the <code>jdk.VirtualThreadPinned</code> JFR event instead
|
||||
(<code>jcmd <pid> JFR.start</code> or a startup <code>-XX:StartFlightRecording</code>).
|
||||
If your JDBC driver documentation still warns about <code>synchronized</code>-based pinning
|
||||
from before your driver's JDK 24 testing, verify against your actual JDK version before
|
||||
believing it.</blockquote>
|
||||
|
||||
- JEP 491 in full, including the exact wording on what still pins:
|
||||
<https://openjdk.org/jeps/491> (`rel=nofollow`)
|
||||
- JEP 444 (virtual threads, JDK 21 baseline): <https://openjdk.org/jeps/444> (`rel=nofollow`)
|
||||
|
||||
Next: [04-scoped-value-and-checklist.md](04-scoped-value-and-checklist.md).
|
||||
@@ -0,0 +1,80 @@
|
||||
# 4. ScopedValue, the JDBC driver advice, and a production checklist
|
||||
|
||||
[Previous: 03-pinning-diagnosis.md](03-pinning-diagnosis.md) | [README](../README.md)
|
||||
|
||||
## ScopedValue is finalized in JDK 25, with a different API than the preview version
|
||||
|
||||
The old post's `ThreadLocal` gotcha recommended `ScopedValue` as "the Project Loom-native
|
||||
replacement," using `ScopedValue.runWhere(CTX, value, () -> { ... })`. Two things changed:
|
||||
|
||||
1. **[JEP 506](https://openjdk.org/jeps/506) (`rel=nofollow`) finalized `ScopedValue` in JDK 25** --
|
||||
it's no longer a preview feature and needs no `--enable-preview` flag. Confirmed by compiling
|
||||
and running a `ScopedValue` example on this repo's JDK 25 with no preview flags at all.
|
||||
2. **`ScopedValue.runWhere(...)` does not exist on the finalized API.** `javap -p java.lang.ScopedValue`
|
||||
against this JDK's runtime classes shows no such method. The finalized shape is
|
||||
`ScopedValue.where(scopedValue, value).run(runnable)` (a `Carrier` returned by `where`, with
|
||||
`.run(...)` or `.call(...)` on it):
|
||||
|
||||
```java
|
||||
private static final ScopedValue<ExpensiveContext> CTX = ScopedValue.newInstance();
|
||||
|
||||
ScopedValue.where(CTX, new ExpensiveContext()).run(() -> {
|
||||
// CTX is accessible in this scope and any method called from here
|
||||
processRequest();
|
||||
// automatically cleared when the scope exits
|
||||
});
|
||||
```
|
||||
|
||||
Code copied from a pre-JDK-25 `ScopedValue` article using `runWhere` will not compile on this
|
||||
JDK. This is a small API surface change, but it's the kind of thing `javap` catches in seconds
|
||||
and a fabricated-from-memory snippet would not.
|
||||
|
||||
## The JDBC driver pinning advice needs the same correction as the general case
|
||||
|
||||
The old post's second gotcha was "many JDBC drivers use `synchronized` internally in their
|
||||
socket I/O paths, so every DB call becomes a pinning event" -- true through JDK 23, and the
|
||||
reason MySQL Connector/J and older PostgreSQL JDBC driver versions got singled out for their
|
||||
internal locking. [Chapter 3](03-pinning-diagnosis.md) already established that ordinary
|
||||
`synchronized`-then-block no longer pins as of JDK 24. That correction applies here without
|
||||
exception: if a driver's pinning was caused by a plain `synchronized` block around socket I/O
|
||||
(the common case), **JEP 491 fixes it for free on JDK 24+, with no driver upgrade required.**
|
||||
The narrower native-callback pinning case from chapter 3 is the only mechanism left that could
|
||||
still affect a driver, and it needs the driver to call native code that calls back into
|
||||
blocking Java -- unusual for a JDBC driver's I/O path. Treat "upgrade your JDBC driver to fix
|
||||
pinning" as obsolete advice on JDK 24+; verify with `jdk.VirtualThreadPinned` JFR events on
|
||||
your actual driver and JDK version rather than assuming either the old warning or this
|
||||
correction applies to your exact setup.
|
||||
|
||||
## Never pool virtual threads -- unchanged
|
||||
|
||||
Nothing about JEP 491 changes this. Virtual threads are cheap to create; pooling them adds
|
||||
synchronization overhead for no benefit and defeats the design:
|
||||
|
||||
```java
|
||||
// WRONG: pooling virtual threads
|
||||
ExecutorService pool = Executors.newFixedThreadPool(100, Thread.ofVirtual().factory());
|
||||
|
||||
// RIGHT: unbounded virtual-thread-per-task executor
|
||||
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||
```
|
||||
|
||||
This repo's own load generator (`LoadBenchmarkTest.fireConcurrent`) uses exactly this pattern
|
||||
for its `HttpClient` executor.
|
||||
|
||||
## Production checklist for Boot 4.1 / JDK 25
|
||||
|
||||
- Enable with `spring.threads.virtual.enabled=true` -- one property, unchanged since Boot 3.2.
|
||||
- Confirm you're actually on JDK 24+ before relying on JEP 491 -- `synchronized` still pins on
|
||||
JDK 21-23.
|
||||
- Delete `-Djdk.tracePinnedThreads=full` from your flags on JDK 24+; it's inert. Use the
|
||||
`jdk.VirtualThreadPinned` JFR event instead.
|
||||
- Profile I/O-bound endpoints for the real win; don't expect anything from CPU-bound work --
|
||||
[chapter 2](02-benchmark-methodology.md) measured this directly, including a benchmarking
|
||||
bug that briefly suggested otherwise.
|
||||
- If you still see pinning on JDK 24+, suspect native-callback code paths (JNI, Foreign
|
||||
Function & Memory API), not plain `synchronized`.
|
||||
- Prefer `ScopedValue` over `ThreadLocal` for new code holding per-request context, using the
|
||||
finalized JDK 25 API (`ScopedValue.where(...).run(...)`, not the old preview `runWhere`).
|
||||
- Never pool virtual threads.
|
||||
|
||||
- Further reading: [JEP 444](https://openjdk.org/jeps/444) (`rel=nofollow`), [JEP 491](https://openjdk.org/jeps/491) (`rel=nofollow`), [JEP 506](https://openjdk.org/jeps/506) (`rel=nofollow`).
|
||||
@@ -0,0 +1,4 @@
|
||||
GET /thread-info with spring.threads.virtual.enabled=false vs true
|
||||
==================================================================
|
||||
enabled=false -> Thread: Thread[#9742,http-nio-auto-5-exec-1,5,main] | Virtual: false
|
||||
enabled=true -> Thread: VirtualThread[#9769,tomcat-handler-0]/runnable@ForkJoinPool-1-worker-1 | Virtual: true
|
||||
@@ -0,0 +1,7 @@
|
||||
I/O-bound endpoint (/io, Thread.sleep(300)), concurrency=600, 2 vCPU sandbox, Spring Boot 4.1.1 / JDK 25.0.4.1
|
||||
==============================================================================================================
|
||||
platform threads : total=600 success=600 wall=1325ms p50=733ms p99=1201ms
|
||||
virtual threads : total=600 success=600 wall=1042ms p50=658ms p99=720ms
|
||||
|
||||
default embedded Tomcat platform-thread pool = 200; with 600 concurrent 300ms-sleep
|
||||
requests, platform threads must queue in ~3 sequential batches, virtual threads do not.
|
||||
@@ -0,0 +1,8 @@
|
||||
CPU-bound endpoint (/cpu, 20,000x SHA-256), concurrency=60, 2 vCPU sandbox, Spring Boot 4.1.1 / JDK 25.0.4.1
|
||||
============================================================================================================
|
||||
platform threads : total=60 success=60 wall=208ms p50=130ms p99=202ms
|
||||
virtual threads : total=60 success=60 wall=201ms p50=187ms p99=196ms
|
||||
|
||||
a CPU-bound virtual thread never yields -- it stays mounted on its carrier for the full
|
||||
computation, so it competes for the same 2 physical cores a platform thread would. Expect
|
||||
these two wall times to be close, not virtual threads winning decisively as in the I/O case.
|
||||
@@ -0,0 +1,9 @@
|
||||
JEP 491 proof: synchronized held across Thread.sleep, JDK 25.0.4.1
|
||||
==================================================================
|
||||
availableProcessors (default virtual-thread carrier pool size) = 2
|
||||
virtual threads = 60, each holds a DISTINCT monitor for 250ms
|
||||
predicted wall time IF PINNED (pre-JDK-24 behaviour): ~7500ms
|
||||
predicted wall time IF NOT PINNED (JDK 24+ behaviour): ~250ms, independent of carrier count
|
||||
|
||||
actual wall time: 251ms
|
||||
verdict: NOT PINNED -- matches JEP 491's documented JDK 24+ behaviour
|
||||
@@ -0,0 +1,18 @@
|
||||
java -version:
|
||||
openjdk version "25.0.4.1" 2026-08-18 LTS
|
||||
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
|
||||
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
|
||||
|
||||
-- without -Djdk.tracePinnedThreads=full --
|
||||
java.version=25.0.4.1
|
||||
jdk.tracePinnedThreads=null
|
||||
Running a virtual thread that holds a monitor across Thread.sleep(200)...
|
||||
Done. If jdk.tracePinnedThreads still worked on this JDK, a pinned-thread stack trace would have printed above while the virtual thread was inside doWorkHoldingMonitor.
|
||||
|
||||
-- with -Djdk.tracePinnedThreads=full --
|
||||
java.version=25.0.4.1
|
||||
jdk.tracePinnedThreads=full
|
||||
Running a virtual thread that holds a monitor across Thread.sleep(200)...
|
||||
Done. If jdk.tracePinnedThreads still worked on this JDK, a pinned-thread stack trace would have printed above while the virtual thread was inside doWorkHoldingMonitor.
|
||||
|
||||
RESULT: no pinned-thread stack trace was printed by either run -- the flag is inert on this JDK, consistent with JEP 491
|
||||
@@ -0,0 +1,4 @@
|
||||
ScopedValue (JEP 506, finalized JDK 25), JDK 25.0.4.1
|
||||
=====================================================
|
||||
ScopedValue.where(CTX, ctx).call(ScopedValueDemo::processRequest) -> handled req-42
|
||||
no --enable-preview flag used on this run
|
||||
Reference in New Issue
Block a user