Scoped Values
Scoped Values, finalized in Java 25 (JEP 506, after several preview iterations starting with JEP 429 in Java 20), provide ScopedValue<T> as a mechanism for safely and efficiently sharing immutable, per-operation data with called methods and, critically, with virtual-thread subtasks forked via structured concurrency — addressing both the mutability hazards and the high per-virtual-thread memory cost that made the older ThreadLocal mechanism a poor fit once an application might be running with enormous numbers of concurrent virtual threads. This entry covers exactly what was wrong with ThreadLocal for this purpose, ScopedValue's bound-for-the-duration-of-a-call model via where()/run()/call(), how scoped value bindings are automatically inherited by subtasks forked in a StructuredTaskScope, and the deliberate restrictions (no general mutation, no unbind-and-rebind in arbitrary code) that give Scoped Values their safety and performance advantages over ThreadLocal.
Motivation — Where ThreadLocal Falls Short for This Purpose
// ── ThreadLocal — mutable, settable from anywhere, at any time ─────────
static final ThreadLocal<String> requestId = new ThreadLocal<>();
void handleRequest() {
requestId.set("req-123"); // set once here...
doSomeWork();
// ... but ANY code, anywhere, with a reference to "requestId" could call
// requestId.set("something-else") at any later point in this same thread's
// execution — no guarantee the value read later is still what was set here
}
void doSomeWork() {
System.out.println(requestId.get()); // relies on trusting nobody changed it in between
}
// ── Scoped Values — bound only for a specific call's dynamic extent ────
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
void handleRequest() {
ScopedValue.where(REQUEST_ID, "req-123")
.run(() -> doSomeWork()); // REQUEST_ID is bound to "req-123" ONLY for this call
// Once run() returns, REQUEST_ID is automatically, unconditionally unbound here —
// no lingering value, no way it could have been silently changed mid-call from elsewhere
}
void doSomeWork() {
System.out.println(REQUEST_ID.get()); // "req-123" — guaranteed stable for this entire call
}
// REQUEST_ID.get(); // called OUTSIDE any where(...).run(...) binding — throws
// NoSuchElementException, since there is no general set()/unbind() to leave a stale value aroundBinding and Reading — where(), run(), call(), and orElse()
static final ScopedValue<String> USER_CONTEXT = ScopedValue.newInstance();
static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
// ── Binding via where().run() / where().call() ──────────────────────────
void processRequest() {
ScopedValue.where(USER_CONTEXT, "alice")
.where(TRACE_ID, "trace-789") // chaining multiple bindings in one Carrier
.run(() -> handleBusinessLogic());
}
String computeSomething() {
return ScopedValue.where(USER_CONTEXT, "bob")
.call(() -> "Processed for " + USER_CONTEXT.get()); // .call() returns a value
}
// ── Reading — get(), isBound(), orElse() ────────────────────────────────
void handleBusinessLogic() {
System.out.println(USER_CONTEXT.get()); // "alice" — bound by the enclosing where().run()
if (TRACE_ID.isBound()) {
System.out.println("Trace: " + TRACE_ID.get());
}
}
void mightNotBeBound() {
// USER_CONTEXT.get(); // NoSuchElementException if called with NO active binding at all
String user = USER_CONTEXT.orElse("anonymous"); // safe fallback, no exception
}
// ── Nested rebinding — restored automatically once the nested call returns
void outer() {
ScopedValue.where(USER_CONTEXT, "alice").run(() -> {
System.out.println(USER_CONTEXT.get()); // "alice"
ScopedValue.where(USER_CONTEXT, "bob").run(() -> {
System.out.println(USER_CONTEXT.get()); // "bob" — nested rebinding in effect
});
System.out.println(USER_CONTEXT.get()); // back to "alice" — outer binding restored
// automatically the instant the nested run() call above returned
});
}Automatic Inheritance by Structured Concurrency Subtasks
// (Scoped Values finalized in Java 25 — JEP 506; Structured Concurrency
// remains a preview feature as of Java 25, requiring --enable-preview)
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
void handleRequest(String requestId) {
ScopedValue.where(REQUEST_ID, requestId).run(() -> {
try (var scope = StructuredTaskScope.open()) {
// Both subtasks below automatically INHERIT REQUEST_ID's binding from
// this owner thread — no explicit parameter needed to pass it along:
var userTask = scope.fork(() -> fetchUserData());
var orderTask = scope.fork(() -> fetchOrderData());
scope.join();
combineResults(userTask.get(), orderTask.get());
}
});
}
String fetchUserData() {
// Running on a DIFFERENT (virtual) thread than handleRequest's owner thread,
// yet REQUEST_ID.get() here returns the SAME value the owner had when the
// scope was opened — inherited automatically, no manual propagation needed:
System.out.println("Fetching user data for request: " + REQUEST_ID.get());
return "user-data";
}
// ── A subtask CAN establish its own further-nested binding, scoped only to itself
String fetchOrderData() {
return ScopedValue.where(REQUEST_ID, REQUEST_ID.get() + "-orders-suffix")
.call(() -> {
// REQUEST_ID here is the rebound value, visible only within THIS nested call —
// sibling subtask "fetchUserData" above is entirely unaffected by this rebinding,
// and the owner thread's own original binding is unaffected too:
return "orders for " + REQUEST_ID.get();
});
}