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.
71 lines
4.5 KiB
Markdown
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).
|