Sitemap
Javarevisited

A humble place to learn Java and Programming better.

Scoped Values -a new way of sharing data in Threads|Java 23

--

Often when we work on large applications or frameworks we need a way to pass the data to the components or tasks called in the call stack without passing the variable as a method or constructor argument. ThreadLocal variables have been used in Java since 1998 to create variables local to a thread and share data across the call stack. However, thread-local variables have several limitations including unconstrained mutability and unbounded lifetime. Scoped Values, a preview feature in Java 23, provides a clean way to share immutable data within a thread and its child threads addressing the limitations of ThreadLocal.

Why do we need Scoped Values?

Thread-local variables have been used in Java since 1998 to create variables local to a thread. Lot of frameworks and libraries use thread-local variables to store context information like user session, transaction context, etc. For example, Spring Security uses thread-local variables to store the current user’s security context.

Although thread-local variables are widely used, they have several limitations including:

  • Unconstrained Mutability: Any code that can call the get method of a thread-local variable can call the set method at any time leading to potential bugs.
  • Unbounded Lifetime: The value of a thread-local variable is retained for the lifetime of the thread, leading to potential memory leaks. When thread pools are used, the thread-local values are not cleared after the thread is returned to the pool. Developers need to manually clear the thread-local values to avoid memory leaks.

Understanding Scoped Values

Scoped values allow you to share immutable data across all components in the call hierarchy without needing to pass it as a method or constructor argument. Scoped values are created within a context and can be accessed by the current thread within that context. If any child threads are created within that context using the new feature StructuredConcurrency, they can also access the same scoped value.

Once the context is over, the value is automatically cleared, avoiding memory leaks. Scoped values are immutable, so they don’t need to be copied across child threads, reducing the memory footprint.

Now let us take an example where ThreadLocal is used to store the user context and see how we can replace it with scoped values.

User Data Sharing using ThreadLocal

public class UserContext {
public static final ThreadLocal<String> user = new ThreadLocal<>();
}

public class UserContextDemo {
public static void main(String[] args) {
List<String> users = List.of("Alice", "Bob", "Charlie", "David", "Eve");

try (ExecutorService executor = Executors.newFixedThreadPool(5)) {
for (String user : users) {
executor.submit(() -> {
try {
UserContext.user.set(user);
var stocks = new StockRepository().getStockSymbols();
} finally {
UserContext.user.remove();
}
});
}
}
}
}

class StockRepository{
Map<String,List<String>> userStocks = Map.of(
"Alice", List.of("AAPL", "GOOGL", "AMZN"),
"Bob", List.of("MSFT", "TSLA"),
"Charlie", List.of("AAPL", "AMZN"),
"David", List.of("GOOGL", "MSFT"),
"Eve", List.of("TSLA")
);
public List<String> getStockSymbols(){
return userStocks.get(UserContext.user.get());
}
}

In the above code, we have a UserContext class that uses a ThreadLocal variable to store the user context. The UserContextDemo class creates threads for different users and sets the user context using ThreadLocal. The StockRepository class uses the user context to fetch the stock symbols for the user.

Here you can see that we have to manually remove the user context using UserContext.user.remove() to avoid memory leaks. Also, the user context is mutable, which can lead to potential bugs.

User Data Sharing using Scoped Values

Now let’s see how we can replace the ThreadLocal with scoped values in Java 22.

  • First we initialize a scoped value using ScopedValue.newInstance()
  • Then we use ScopedValue.runWhere() to set the value of the scoped value within a context. The value set in the scoped value is accessible to the current thread and any child threads created within that context (while using StructuredConcurrency). This can be accessed using ScopedValue.get().
  • Once the context is over, the value is automatically cleared.
  • Scoped values are immutable, so they don’t need to be copied to child threads, reducing the memory footprint.
public class UserContext {
public static final ScopedValue<String> userScopedVal = ScopedValue.newInstance();
}
public class UserContextDemo {
public static void main(String[] args) {
List<String> users = List.of("Alice", "Bob", "Charlie", "David", "Eve");
try(ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()){
for (String user : users) {
executor.submit(() -> {
ScopedValue.runWhere(UserContext.userScopedVal, user, () -> {
var stocks = new StockRepository().getStockSymbols();
});
});
}
}
}
}

class StockRepository{
Map<String,List<String>> userStocks = Map.of(
"Alice", List.of("AAPL", "GOOGL", "AMZN"),
"Bob", List.of("MSFT", "TSLA"),
"Charlie", List.of("AAPL", "AMZN"),
"David", List.of("GOOGL", "MSFT"),
"Eve", List.of("TSLA")
);
public List<String> getStockSymbols(){
return userStocks.get(UserContext.userScopedVal.get());
}
}

Rebinding Scoped Values

The ScopedValue API allows rebinding, which means a ScopedValue can be temporarily bound to a new value within a nested dynamic scope. When that scope completes, the ScopedValue reverts to its previous value.

ScopedValue.runWhere(UserContext.userScopedVal, "Alice", () -> {
var stocks = new StockRepository().getStockSymbols();
// User is Alice
ScopedValue.runWhere(UserContext.userScopedVal, "Bob", () -> {
// User is Bob
var stocks = new StockRepository().getStockSymbols();
});
// User is Alice
});

Inheritance of Scoped Values

Scoped values support inheritance when used along with Structured Concurrency. You can learn more about Structured Concurrency in our blog on .

While using Structured Concurrency, all the child threads created using scope.fork() inherit the scoped values set in the parent thread. This allows you to share data across multiple threads in a structured and safe way. In the below case all 3 child threads will have access to the same user context.

ScopedValue.runWhere(UserContext.userScopedVal, user, () -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
scope.fork(() -> childTask1());
scope.fork(() -> childTask2());
scope.fork(() -> childTask3());
}
});

Conclusion

Scoped values provide an efficient way of handling thread local variables and allows us to write cleaner and safer code.

To stay updated with the latest updates in Java and Spring, follow us on , , and Medium.

Video Version

You can watch the video version of this blog on our YouTube channel.

References

Originally published at .

Javarevisited
Javarevisited

Published in Javarevisited

A humble place to learn Java and Programming better.

Code Wiz
Code Wiz

Written by Code Wiz

Software engineering and programming tutorials and blogs. Website - Youtube -

No responses yet