Java: Scoped Values
7 min read1. Introduction
Scoped Values provides a secure way of sharing data within and across threads, that was introduced as part of JEP 429, to solve a common challenge in concurrent programming. Scoped Values are Java's approach to providing a simple, immutable, and inheritable mechanism for data sharing. In the realm of multithreaded applications, ensuring thread safety and data consistency is paramount. Scoped Values address this by allowing data to be shared in a controlled manner.
Key Characteristics
- Immutability: Once a scoped value is created, it cannot be modified. This immutability is crucial for avoiding concurrency-related issues like race conditions.
- Inheritance: Scoped values can be inherited by child threads, ensuring consistency across thread hierarchies.
- Simplicity: They offer a more straightforward alternative to traditional concurrency mechanisms like thread-local variables.
2. Why Scoped Values?
In the Java programming language, data is usually passed to a method by means of a method parameter, where it may need to be passed through a sequence of many methods to get to the method that makes use of the data. That is to say, every method in the sequence of calls needs to declare the parameter and every method has access to the data.
ScopedValue provides a means to pass data to a faraway method (typically a callback) without using method parameters. It is "as if" every method in a sequence of calls has an additional parameter. None of the methods declare the parameter and only the methods that have access to the ScopedValue object can access the value.
3. Scoped Values
3.1 See in action
private static final ScopedValue<String> NAME = ScopedValue.newInstance();
ScopedValue.runWhere(NAME, "duke", () -> doSomething());
ScopedValue.runWhere(NAME, "duke2", () -> doSomething());
From the example, ScopedValue makes it possible to securely pass data from caller (this function) to a faraway callee (doSomething
)
through a sequence of intermediate methods that do not declare a parameter for the data and have no access to the data.
3.2 Rebinding
Scoped values are immutable.
In some more complex use cases, where different contextual data is required under corresponding scopes,
"rebinding" becomes a critical feature, to allows a new binding to be established for nested dynamic scopes.
In the above example, suppose that code executed by doSomething
binds NAME
to a new value with:
ScopedValue.runWhere(NAME, "duchess", () -> doMore());
Code executed directly or indirectly by doMore()
that invokes NAME.get()
will read the value "duchess".
When doMore()
completes, the original value will be available again (the value of NAME is "duke").
3.3 Inheritance
ScopedValue supports sharing across threads. This sharing is limited to structured cases where child threads are started and terminate within the bounded period of execution by a parent thread. When using a StructuredTaskScope, scoped value bindings are captured when creating a StructuredTaskScope and inherited by all threads started in that task scope with the fork method.
private static final ScopedValue<String> NAME = ScopedValue.newInstance();
ScopedValue.runWhere(NAME, "duke", () -> {
try (var scope = new StructuredTaskScope<String>()) {
scope.fork(() -> childTask1());
scope.fork(() -> childTask2());
scope.fork(() -> childTask3());
...
}
});
In this code example, the ScopedValue NAME is bound to the value "duke" for the execution of a runnable operation. The code in the run method creates a StructuredTaskScope that forks three tasks. Code executed directly or indirectly by these threads running childTask1(), childTask2(), and childTask3() that invokes NAME.get() will read the value "duke".
4. Prior to "Scoped Value"
In Java application that runs multiple components and layers in different threads, and that needs to share data within between, from earlier Java implementations, people usually use a few approaches:
4.1 ThreadLocal
ThreadLocal
commonly used to maintain thread-specific data that is not shared between threads.
Thread-local variables usually have a publicly available declaration that is able to access across components.
For example, the class below generates unique identifiers local to each thread.
A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.
public class ThreadId {
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID
public static int get() {
return threadId.get();
}
}
These variables are unique to each thread that accesses them, where each thread has its own, independently initialized copy of the variable. Note that after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
A ScopedValue excels over a ThreadLocal in cases where the goal is "one-way transmission" of data without specifying method parameters. However, thread-safety and performance should be properly evaluated. For instance:
- ThreadLocal does not prevent code in a faraway callee from setting a new value.
- A ThreadLocal has an unbounded lifetime even after a method completes unless explicitly removed.
- Inheritance is expensive - the map of thread-locals to values must be copied when creating each child thread.
4.2 InheritableThreadLocal
InheritableThreadLocal
is to provide inheritance of values from parent thread to child thread.
This is useful when someone want to pass data from a parent thread to a child thread.
public class InheritableThreadLocalExample {
private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
inheritableThreadLocal.set("Parent thread value");
Thread childThread = new Thread(() -> System.out.println(inheritableThreadLocal.get()));
childThread.start();
}
}
Not exactly comparable to the ScopedValue due to the purposes and performance consideration.
4.3 Synchronization, Locks and thread-safe data structures
Synchronization and locks focusing on using synchronized blocks or explicit locks (like ReentrantLock) to control access to shared resources. Thread-safe data structures designed for concurrent access and modification between various threads. They are focusing on thread safety and concurrency control, but not offering thread-local storage.
5. ScopedValue in Other Languages
ScopedValue is fairly generic concept that is applicable in various programming languages and libraries. Scoped values are often used in languages like Python, C++, Go etc., with constructs like context managers and RAII (Resource Acquisition Is Initialization) to manage resources, such as file handles, database connections, or locks, safely and automatically.
5.1 Python
The contextvars module in Python 3.7+ offers a way to manage context-local state, similar to scoped values but with more focus on asynchronous tasks.
from contextvars import ContextVar
import asyncio
# Define a context variable
ctx_var = ContextVar('example_var', default='Default Value')
async def main():
# Set a new value for the context
ctx_var.set('New Value')
print(ctx_var.get()) # Outputs 'New Value'
# Spawn a new task
await asyncio.create_task(sub_task())
async def sub_task():
# This will inherit the context of the parent task
print(ctx_var.get()) # Outputs 'New Value'
asyncio.run(main())
5.2 Go
Go’s goroutines and channels provide a different approach to concurrency, focusing on message passing rather than shared state.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
messageChan := make(chan string)
wg.Add(1)
go func() {
defer wg.Done()
messageChan <- "Shared Message"
}()
wg.Add(1)
go func() {
defer wg.Done()
msg := <-messageChan
fmt.Println(msg)
}()
wg.Wait()
}
6. Real-world Application Example
Use case: In an enterprise web service application, every incoming HTTP request is being processed in its own thread. Often, it's necessary to include context-specific information, like a unique requestId, to allow trace the flow of a particular request/response throughout the process.
One common practice is to pass along context through multiple layers and methods, that clutter the method signatures.
With ScopedValue, here's how it looks:
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
public void handleRequest(Request request) {
// generateRequestId could generate a random UUID for example.
String requestId = generateRequestId();
ScopedValue.runWhere(REQUEST_ID, requestId, () -> processRequest(request));
}
public class RequestProcessor {
public void performTask() {
String requestId = REQUEST_ID.get();
log("Executing task for request: " + requestId);
// Additional processing
}
}
This example illustrates how ScopedValue can be a powerful tool for implicit, thread-safe data passing in complex, multi-layered Java applications, particularly for logging purposes where contextual information needs to be available across various components of the application.
Simplified Context Passing: ScopedValue eliminates the need for passing the request ID through each method call, leading to cleaner code. Thread Safety: Each thread has its own binding to the ScopedValue, ensuring correct and isolated logging context per request. Dynamic Scope: The value is available throughout the dynamic scope of the request handling, reverting back once the request processing is completed.
7. Conclusion
Scoped Values in Java provide a significant advancement in handling data in multithreaded environments, by offering a secure, simple, and consistent way to share immutable data across threads, enhancing both the safety and readability of concurrent Java applications. Happy coding!