Files
asmhatre f506b01389 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.
2026-09-18 08:52:59 +00:00

71 lines
4.5 KiB
Markdown

# virtual-threads-benchmark
Companion module for the ankurm.com post **"Virtual Threads on Spring Boot 4.1: The
Benchmarks, Re-Run, and the Pinning Advice That Expired."** Every number is from a real
concurrent load run against a real running embedded server on this JDK; every pinning claim is
verified against JDK 25's actual runtime behaviour, not carried forward from the JDK 21-era
post it replaces.
Lives in this container repo (not as its own top-level repository) alongside
[`../async`](../async), the companion module for the `@Async`-focused post, which already has a
dual-JDK JEP 491 pinning proof of its own (see the note near the bottom of this file).
## Versions (verified against `repo1.maven.org` maven-metadata.xml and the OpenJDK JEP pages, not aggregators)
| Component | Version | Notes |
|---|---|---|
| JDK | 25 (Temurin 25.0.4.1+1) | latest LTS; includes JEP 491 (GA in JDK 24) and JEP 506 (GA in JDK 25) |
| Spring Boot | 4.1.1 | latest GA at time of writing |
| Spring Framework | 7.0.9 | latest GA |
## Quickstart
```bash
./scripts/run-all.sh # regenerates every file in docs/output/ from a real test run
./scripts/run.sh # start on :8080, platform threads
./scripts/run.sh vt # start on :8080, spring.threads.virtual.enabled=true
```
Requires JDK 25 and Maven. First run must be online (Maven needs to fetch plugins into the
local cache); `-o` works for subsequent builds. This repo's own benchmark ran on a 2 vCPU
sandbox -- see [docs/02-benchmark-methodology.md](docs/02-benchmark-methodology.md) for why
that matters and how the numbers were still kept honest.
## What's demonstrated where
| Area | Source | Test | Transcript |
|---|---|---|---|
| `spring.threads.virtual.enabled` actually changes the request thread type | [`DemoController`](src/main/java/com/ankurm/vthreads/DemoController.java) | [`ThreadTypeTest`](src/test/java/com/ankurm/vthreads/ThreadTypeTest.java) | [`00`](docs/output/00-thread-type-confirmation.txt) |
| I/O-bound throughput: platform vs virtual threads, 600 concurrent | same | [`LoadBenchmarkTest`](src/test/java/com/ankurm/vthreads/LoadBenchmarkTest.java) | [`01`](docs/output/01-io-bound-benchmark.txt) |
| CPU-bound throughput: platform vs virtual threads, 60 concurrent (plus the JIT-warmup benchmarking bug this repo caught and fixed) | same | same | [`02`](docs/output/02-cpu-bound-benchmark.txt) |
| JEP 491 proof: `synchronized` no longer pins across a blocking sleep on JDK 24+ | [`PinningDemoService`](src/main/java/com/ankurm/vthreads/PinningDemoService.java) | [`PinningJep491Test`](src/test/java/com/ankurm/vthreads/PinningJep491Test.java) | [`03a`](docs/output/03a-pinning-jep491-proof.txt) |
| `-Djdk.tracePinnedThreads=full` is inert on JDK 25 | [`PinningTraceCheckMain`](src/main/java/com/ankurm/vthreads/PinningTraceCheckMain.java) | [`check-trace-pinned-threads-removed.sh`](scripts/check-trace-pinned-threads-removed.sh) | [`03b`](docs/output/03b-trace-pinned-threads-removed.txt) |
## Documentation chapters
1. [Enabling virtual threads on Spring Boot 4.1](docs/01-enabling-virtual-threads.md) -- the one
property, what it actually flips, and a `SpringApplicationBuilder` property-precedence trap
this repo's own benchmark hit
2. [Benchmark methodology and results](docs/02-benchmark-methodology.md) -- I/O-bound and
CPU-bound, on this sandbox's real hardware, including a JIT-warmup measurement bug caught
and fixed mid-build
3. [Pinning diagnosis, corrected for JEP 491](docs/03-pinning-diagnosis.md) -- why the standard
`synchronized`-pins-your-carrier advice, and the `-Djdk.tracePinnedThreads=full` flag every
article tells you to use, both stopped being true in JDK 24
4. [ScopedValue, the JDBC driver advice, and a production checklist](docs/04-scoped-value-and-checklist.md)
## A note on this module's relationship to `../async`
[`../async`](../async) is the companion module for a different, `@Async`-focused ankurm.com
post and already contains a dual-JDK (21.0.12.1 vs 25.0.4.1) proof of the same JEP 491 pinning
change, which is stronger evidence than this module can offer on its own (this module only had
JDK 25 available to test against). [Chapter 3](docs/03-pinning-diagnosis.md) here cross-links
to it rather than duplicating it. This module's own scope -- enabling the flag, the I/O/CPU
throughput benchmark, and the diagnosis flag's removal -- doesn't overlap with that module's
`@Async`-specific material, which is why it's kept as its own module here rather than merged
into `async/`.
## License
MIT -- see [LICENSE](LICENSE).