# 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 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`).