39. ScopedValue vs ThreadLocal
ScopedValue (Java 21+) is the modern, structured alternative to ThreadLocal โ immutable within a scope, works correctly with virtual threads, and automatically cleaned up.
Explanation
ThreadLocal stores a value per thread, mutable at any time, and never automatically removed (requiring explicit remove() to avoid memory leaks in thread pools). It also doesn't compose well with virtual threads โ a virtual thread may run on many carrier threads, but ThreadLocal still works (the value follows the virtual thread); however, the inheritableThreadLocal semantics can be surprising.
ScopedValue (JEP 446) provides a value that is bound within a structured scope and is immutable โ it cannot be accidentally overwritten. It is automatically unbound when the scope exits. This makes it safe for structured concurrency's subtask trees.
Pattern: declare as a static final ScopedValue<T>, then bind with ScopedValue.where(KEY, value).run(() -> ...). Inside the runnable, KEY.get() returns the bound value. Subtasks (forked within the scope) inherit the binding.
Example
import java.util.concurrent.StructuredTaskScope;
public class ScopedValueDemo {
// Declare as static final (like a ThreadLocal)
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
static void processRequest(String user, String reqId) throws Exception {
// Bind values for the duration of this scope
ScopedValue.where(CURRENT_USER, user)
.where(REQUEST_ID, reqId)
.run(() -> {
try { handleRequest(); }
catch (Exception e) { throw new RuntimeException(e); }
});
}
static void handleRequest() throws Exception {
// Accessible anywhere in the call tree within the scope
String user = CURRENT_USER.get();
String reqId = REQUEST_ID.get();
System.out.printf("[%s] Handling request for user %s%n", reqId, user);
// Works inside structured concurrency subtasks too
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> {
System.out.printf("[%s] Subtask running as %s%n",
REQUEST_ID.get(), CURRENT_USER.get());
return null;
});
scope.join().throwIfFailed();
}
}
public static void main(String[] args) throws Exception {
processRequest("alice", "req-001");
processRequest("bob", "req-002");
// Outside the scope: isBound() returns false
System.out.println("Bound outside scope: " + CURRENT_USER.isBound()); // false
}
}Sources
- https://openjdk.org/jeps/446 โ JEP 446: Scoped Values (preview Java 21)
- https://dev.java/learn/new-features/scoped-values/ โ Scoped values guide