Often when you work on large applications or frameworks you 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
.
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:
get
method of a thread-local variable can call the set
method at any time leading to potential bugs.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.
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.
Now let's see how we can replace the ThreadLocal
with scoped values in Java 22.
ScopedValue.newInstance()
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()
.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()); } }
The ScopedValue
API allows rebinding, which means a ScopedValue
can be temporarily bound to a new value within a nested dynamic scope. When the method 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 });
Scoped values support inheritance when used along with Structured Concurrency. You can learn more about Structured Concurrency in our blog on Concurrency in Spring and Java.
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()); } });
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 YouTube, LinkedIn, and Medium.
You can watch the video version of this blog on our YouTube channel.
This blog introduces Stream Gatherers, a new feature in Java 22, which allows developers to add custom intermediate operations to stream processing. It explains how to create and use Stream Gatherers to enhance data transformation capabilities in Java streams.
Learn how to run tasks in parallel in Java and Spring Boot applications using CompletableFuture, @Async annotation, Virtual Threads and Structured Concurrency. We will see how to improve the performance of the application by running tasks in parallel.
Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.