跪拜 Guibai
← All articles
Java · Architecture · Backend

ScopedValue Graduates in Java 25: The JDK's New Answer to Context Propagation

By 一只叫煤球的猫 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Any Java service that threads a request context through layers of business logic with ThreadLocal now has a standard, safer alternative that prevents leaks and accidental mutation. The shift is especially relevant as virtual threads make per-thread state expensive; ScopedValue’s pointer-like inheritance cost keeps context propagation cheap at scale.

Summary

ScopedValue has graduated from preview in Java 25 after four rounds of refinement, offering a structured replacement for the common but error-prone practice of using ThreadLocal to pass UserContext, TraceId, and similar data down a call stack. The API shifted from static `runWhere`/`callWhere` methods to a fluent `ScopedValue.where(...).run(...)` style in Java 24, and Java 25 tightened semantics by disallowing `orElse(null)`. Bindings are immutable within a scope, automatically restored when the scope exits, and inherited by virtual threads in structured concurrency at a cost closer to copying a pointer than cloning a full ThreadLocal map.

The design directly addresses three documented ThreadLocal shortcomings: unbounded lifecycle requiring manual `remove()`, unrestricted mutability by any downstream code, and the high inheritance cost of `InheritableThreadLocal`. ScopedValue treats context as an implicit final parameter belonging to a call chain, not as thread-owned mutable state. The JDK team’s direction is now explicit: ThreadLocal remains for genuine thread-local mutable state, while one-way context propagation moves to ScopedValue.

When combined with virtual threads and the still-preview Structured Concurrency API, child tasks forked inside a `StructuredTaskScope` automatically inherit the parent’s ScopedValue bindings, removing entire categories of `set`/`copy`/`remove` boilerplate from concurrent code.

Takeaways
ScopedValue graduated from preview to a stable API in Java 25 (JEP 506).
The final API uses a fluent style: `ScopedValue.where(CONTEXT, value).run(() -> ...)`.
Bindings are immutable within a scope and automatically discarded when the scope exits, even on exceptions.
ScopedValue has no public `set()` method; creating a new binding requires opening a nested scope.
Java 25 disallows `orElse(null)`, tightening the API’s null-safety semantics.
Virtual threads inherit ScopedValue bindings at a cost roughly equivalent to copying a pointer, not duplicating a ThreadLocal map.
ThreadLocal is not deprecated; it remains the right choice for thread-local mutable state, while ScopedValue targets one-way call-chain context propagation.
Structured Concurrency (still in preview) lets child tasks automatically inherit ScopedValue bindings from the parent scope.
Conclusions

The JDK’s decision to stop evolving ThreadLocal and instead extract context propagation into ScopedValue signals that the old API had accumulated responsibilities it was never designed for.

Removing `runWhere`/`callWhere` in favor of a fluent `where(...).run(...)` chain makes multi-context binding read like a declarative scope definition rather than a nested function call.

Disallowing `orElse(null)` is a small but telling change: it pushes developers toward explicit default handling instead of silently propagating nulls through a call chain.

The pointer-like inheritance model for virtual threads means ScopedValue was built for the million-thread reality from day one, unlike InheritableThreadLocal which was designed for modest thread counts.

ScopedValue’s immutability within a scope enforces a functional style that prevents the kind of spooky-action-at-a-distance bugs common with ThreadLocal-based context holders.

Concepts & terms
ScopedValue
A Java 25 API that binds an immutable value to a dynamic scope (a call chain). The binding is automatically discarded when the scope exits, and child threads can inherit it at low cost.
Virtual Thread
Lightweight threads introduced in Java 21 (JEP 444) that allow millions of concurrent threads. They make per-thread state like ThreadLocal maps expensive, motivating ScopedValue’s pointer-like inheritance.
Structured Concurrency
A preview API (`StructuredTaskScope`) that treats groups of related tasks as a single unit of work. Child tasks automatically inherit ScopedValue bindings from the parent scope.
InheritableThreadLocal
A ThreadLocal subclass that copies parent-thread values to child threads at creation time. Its per-thread copy cost becomes prohibitive with large numbers of virtual threads.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗