Scoped Values and Thread-Local Alternatives
We have explored how structured concurrency makes multithreaded code safer and easier to manage. Now we look at a new primitive in the Java concurrency toolbox: ScopedValue
. This feature replaces many use cases of ThreadLocal
with a model better suited for virtual threads.
What Is ScopedValue?
ScopedValue
is a safe, immutable, and inheritable thread context designed to replace ThreadLocal
in virtual-thread environments. Unlike ThreadLocal
, which relies on mutable state tied to the thread, ScopedValue
is a single-assignment object bound to a well-defined scope.
You define a value in a scope, and all code within that scope — including tasks running in virtual threads — can access it.
Why Not ThreadLocal?
ThreadLocal
has long been used for per-thread data like user sessions, log context, and request-scoped state. But in the virtual thread world, it poses problems:
- It’s mutable, leading to subtle bugs in shared environments
- It leaks memory if not cleaned up properly
- It’s expensive to maintain across millions of virtual threads
ScopedValue
solves these issues by being immutable and scope-bound.
Basic Usage
static final ScopedValue<String> USER = ScopedValue.newInstance();
void handleRequest(String userId) {
ScopedValue.where(USER, userId).run(() -> {
logRequest(); // prints: user=alice
processRequest(); // can access USER.get()
});
}
Inside the ScopedValue.where(...).run()
block, the value is safely accessible. Outside, it throws IllegalStateException
if you try to read it.
ScopedValue vs ThreadLocal Comparison
Feature | ThreadLocal | ScopedValue |
---|---|---|
Mutable | Yes | No |
Garbage-safety | Manual cleanup needed | Auto scoped |
Virtual thread friendly | No | Yes |
Default value support | Yes | No (must be explicitly set) |
Inheritance | Yes (but buggy) | Explicit |
ScopedValue and Structured Concurrency
ScopedValue
works naturally with StructuredTaskScope
. For example:
ScopedValue.where(CTX, "alice").run(() -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> logContext()); // "alice"
scope.fork(() -> audit()); // "alice"
scope.join();
}
});
All subtasks share the same value safely without mutable thread-local state.
Best Practices
- Use
ScopedValue
for all new context-passing code - Avoid
ThreadLocal
in virtual thread-based systems - Use
ScopedValue.get()
only insideScopedValue.where(...)
blocks
Conclusion
ScopedValue
is a modern replacement for ThreadLocal
that aligns with virtual threads, structured concurrency, and clean context propagation. It brings safety and immutability to multithreaded environments and avoids common pitfalls of legacy thread-local storage.
No comments:
Post a Comment