VOOZH about

URL: https://www.javacodegeeks.com/2025/11/scoped-values-the-modern-alternative-to-threadlocal-that-java-developers-have-been-waiting-for.html

⇱ Scoped Values: The Modern Alternative to ThreadLocal That Java Developers Have Been Waiting For - Java Code Geeks


If you’ve been writing multithreaded Java applications, you’ve probably encountered ThreadLocal variables. They’ve been around since Java 1.2—over 25 years!—helping developers share data within threads without passing it through every method parameter. But times change, and so does Java. With the advent of virtual threads and Project Loom, the old ThreadLocal approach shows its age. Enter Scoped Values: a sleek, efficient, and safer alternative that’s reshaping how we think about thread-local data.

👁 Image
Source: https://codewiz.info/blog/scoped-values/

The Problem with ThreadLocal: Why Change Was Needed

Picture this: You’re building a web application that handles thousands of concurrent requests. Each request needs to carry user authentication details throughout its processing lifecycle—from the web controller through service layers to data access components. Traditionally, you’d use ThreadLocal for this.

public class ThreadLocalExample {
 private static final ThreadLocal<String> userContext = new ThreadLocal<>();
 
 public static void main(String[] args) {
 userContext.set("Admin");
 System.out.println("User: " + userContext.get());
 userContext.remove(); // DON'T FORGET THIS!
 }
}

But here’s the catch: ThreadLocal comes with baggage that’s increasingly problematic in modern applications:

1. Memory Leak Landmines

You must manually call remove() or risk memory leaks. Forget to clean up? Your application slowly bleeds memory as threads accumulate abandoned values. It’s like leaving the lights on in every room you’ve ever entered—eventually, you’ll overload the circuit.

2. Mutability Chaos

ThreadLocal values can be changed anywhere, anytime. This “spooky action at a distance” (borrowing Einstein’s phrase about quantum mechanics) makes debugging a nightmare. Which method modified the value? When? Why? Good luck tracing that through a complex call chain.

3. Expensive Inheritance

When a parent thread spawns child threads using InheritableThreadLocal, the entire thread-local map gets copied. With virtual threads—where you might have 100,000 concurrent threads—this memory pressure becomes unbearable.

4. Lifetime Ambiguity

ThreadLocal values persist for the thread’s entire lifetime unless explicitly removed. There’s no clear boundary showing where data should be accessible and where it shouldn’t.

Enter Scoped Values: The Hero We Needed

Introduced in Java 20 as an incubator feature and refined through Java 21 and 22, Scoped Values address every pain point of ThreadLocal while being optimized for the virtual thread era.

What Makes Scoped Values Different?

Think of Scoped Values as ThreadLocal’s smarter, more disciplined younger sibling. Here’s the fundamental shift:

import java.lang.ScopedValue;

public class ScopedValuesExample {
 private static final ScopedValue<String> USER_CONTEXT = ScopedValue.newInstance();
 
 public static void main(String[] args) {
 ScopedValue.where(USER_CONTEXT, "Admin").run(() -> {
 System.out.println("User: " + USER_CONTEXT.get());
 processRequest(); // Value available in nested calls
 });
 // USER_CONTEXT is automatically cleared here!
 // No manual cleanup needed
 }
 
 private static void processRequest() {
 System.out.println("Still accessible: " + USER_CONTEXT.get());
 }
}

What just happened here? The value “Admin” is bound to USER_CONTEXT only within the run() lambda’s scope. Once that scope ends, the value automatically disappears. No manual cleanup. No memory leaks. Just clean, predictable behavior.

The Core Principles: Why Scoped Values Work Better

1. Immutability by Design

Once you bind a value to a ScopedValue, it cannot be changed within that scope. This isn’t a limitation—it’s a feature. It eliminates entire categories of bugs caused by unexpected mutations.

ScopedValue.where(USER_CONTEXT, "Admin").run(() -> {
 // No set() method exists!
 // The value remains "Admin" throughout this scope
 callServiceLayer();
 callDataAccessLayer();
 // All methods see the same, unchanging value
});

2. Explicit, Bounded Lifetime

The syntactic structure of your code reveals exactly when data is accessible. Look at those curly braces? That’s your scope. That’s where the value lives. After that block ends, it’s gone.

This isn’t just about memory management—it’s about cognitive load. You can understand data flow at a glance. No hunting through code to find where ThreadLocal values might be mutated or cleared.

3. Blazing Fast Performance

The JVM can optimize scoped value access aggressively because of immutability. Reading a scoped value with get() is often as fast as reading a local variable, regardless of how deeply nested your method calls are. The implementation uses a lightweight caching mechanism that makes repeated access nearly free.

For virtual threads, this is crucial. With potentially millions of concurrent threads, every byte and every CPU cycle matters.

4. Zero-Cost Inheritance

When you use Scoped Values with Structured Concurrency (another Java 21 preview feature), child threads automatically inherit parent values. But here’s the magic: because values are immutable, there’s no copying overhead. It’s essentially just passing a pointer.

private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

ScopedValue.where(REQUEST_ID, "REQ-12345").run(() -> {
 try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
 // Child threads automatically see REQUEST_ID
 scope.fork(() -> processPartA());
 scope.fork(() -> processPartB());
 scope.join();
 }
});

Both processPartA() and processPartB() see “REQ-12345” without any copying. Try that efficiently with ThreadLocal!

Real-World Example: Building a Web Framework Context

Let’s see how Scoped Values shine in a practical scenario—handling web requests with user authentication and transaction management:

public class WebFramework {
 private static final ScopedValue<Principal> LOGGED_IN_USER = ScopedValue.newInstance();
 private static final ScopedValue<Connection> DB_CONNECTION = ScopedValue.newInstance();
 
 public void handleRequest(Request request) {
 Principal user = authenticate(request);
 Connection conn = getConnection();
 
 ScopedValue.where(LOGGED_IN_USER, user)
 .where(DB_CONNECTION, conn)
 .run(() -> {
 processRequest(request);
 });
 
 // conn and user automatically cleaned up
 }
 
 private void processRequest(Request request) {
 // Any method in the call chain can access these values
 Principal currentUser = LOGGED_IN_USER.get();
 Connection db = DB_CONNECTION.get();
 
 // Business logic here
 serviceLayer.process();
 dataAccessLayer.save();
 }
}

Notice what we didn’t do:

  • No passing user and conn through every method parameter
  • No manual cleanup code
  • No risk of another method mutating these values
  • No memory leaks if an exception is thrown

The framework handles a request, binds necessary context, processes the request through various layers, and automatically cleans up when done. Beautiful.

Multiple Threads: Random Number Generation Example

Here’s a complete example showing how different threads get their own scoped values:

import java.lang.ScopedValue;
import java.util.concurrent.Executors;

public class MultiThreadExample {
 private static final ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();
 
 public static void main(String[] args) {
 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
 for (int i = 0; i < 10; i++) {
 executor.submit(() -> {
 int randomValue = (int) (Math.random() * 100);
 ScopedValue.where(RANDOM_NUMBER, randomValue).run(() -> {
 System.out.println(Thread.currentThread().getName() + 
 ": Random number: " + RANDOM_NUMBER.get());
 doSomeWork();
 });
 });
 }
 }
 }
 
 private static void doSomeWork() {
 // Can still access RANDOM_NUMBER here
 System.out.println("Working with: " + RANDOM_NUMBER.get());
 }
}

Each virtual thread gets its own random number, accessible throughout its scope. Simple, clean, efficient.

When to Use Scoped Values vs. ThreadLocal

Use Scoped Values when:

  • You’re working with virtual threads (Project Loom)
  • You need to share immutable context data (user info, request IDs, transaction context)
  • You want automatic cleanup and bounded lifetimes
  • You’re using structured concurrency and need efficient child thread inheritance
  • You want code that’s easier to understand and maintain

Stick with ThreadLocal when:

  • You need truly mutable per-thread storage
  • You’re caching expensive-to-create objects (like DateFormat instances)
  • You’re working with legacy systems that can’t be refactored
  • Your data genuinely needs to persist for the thread’s entire lifetime

The Technical Deep Dive: How It Actually Works

Under the hood, Scoped Values use a sophisticated but lightweight implementation involving two key components:

  1. Carrier: Holds the binding between a ScopedValue and its actual value
  2. Snapshot: Captures the state of all bindings at a particular point

When you call ScopedValue.where(KEY, value).run(...), the JVM creates a new carrier linking that key-value pair. The ScopedValue object itself acts like a map key—a unique pointer to find the value in the carrier stack.

The genius is in the caching: The first time you call get(), it searches through enclosing scopes to find the binding. Then it caches the result in a small thread-local cache. Subsequent accesses are blazingly fast—potentially as fast as reading a local variable.

Pro tip: If you need to bind multiple values, create a record class to hold them and bind a single ScopedValue to that record instance. This maximizes cache efficiency:

record RequestContext(Principal user, Connection db, String requestId) {}

private static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();

ScopedValue.where(CONTEXT, new RequestContext(user, conn, "REQ-123"))
 .run(() -> processRequest());

Current Status and Future

As of November 2025, Scoped Values have progressed from incubator (Java 20) through preview status (Java 21, 22) and are on track to become a permanent feature. The API has remained stable through multiple preview cycles, indicating it’s nearly ready for prime time.

To use Scoped Values in Java 21-23, you currently need to enable preview features:

javac --release 23 --enable-preview YourProgram.java
java --enable-preview YourProgram

Once finalized (likely Java 24 or 25), the preview flags won’t be necessary.

The Bottom Line

Scoped Values represent a fundamental rethinking of how we share data in multithreaded Java applications. By embracing immutability, explicit scoping, and performance optimization, they address every major weakness of ThreadLocal while being perfectly suited for the virtual thread revolution.

You don’t need to rush out and replace every ThreadLocal in your codebase. But for new code—especially code designed for virtual threads and structured concurrency—Scoped Values should be your default choice. They’re safer, faster, clearer, and more maintainable.

The message is clear: For new Java applications targeting virtual threads and structured concurrency, Scoped Values should be your go-to choice for sharing context data. They make your code safer, faster, and dramatically easier to understand.

The future of Java concurrency is here, and it’s beautifully scoped.

What We’ve Learned in This Article

After more than 25 years of service, ThreadLocal shows clear limitations in modern Java applications. Manual cleanup requirements create memory leak risks, mutability allows unexpected state changes, inheritance is expensive, and lifetimes are often unclear—problems that become especially apparent when working with virtual threads.

Scoped Values address these issues through their core design. They provide automatic lifecycle management that eliminates memory leak concerns, immutability that prevents unexpected mutations, and bounded scopes that make data lifetimes explicit. Performance is strong, with fast access enabled by optimized caching and efficient inheritance that allows zero-cost sharing with child threads.

The programming model centers on explicit scopes. Values are bound using ScopedValue.where(KEY, value).run(...), automatically accessible throughout nested method calls, and cleaned up automatically when the scope ends. This approach integrates well with Virtual Threads (JEP 444) and Structured Concurrency (JEP 453), creating a coherent foundation for modern concurrent programming.

Performance characteristics are practical for real-world use. Reading scoped values approaches the speed of local variables through smart caching, and the lightweight implementation handles millions of virtual threads without the memory pressure that affects ThreadLocal. Common use cases include web request contexts, user authentication data, transaction management, request tracing, and any scenario requiring immutable context propagation through call chains.

The choice between Scoped Values and ThreadLocal depends on your needs. Scoped Values fit well for immutable context sharing in modern code, while ThreadLocal remains appropriate for mutable per-thread caching and legacy systems. The feature has evolved from incubator status in Java 20 through preview phases in Java 21-23, with API stability suggesting it will soon become a permanent feature.

When binding multiple values, grouping them in a record class and binding a single ScopedValue maximizes cache efficiency and simplifies code. More broadly, Scoped Values represent a shift toward safer and more understandable concurrent programming, aligning with Java’s direction toward lightweight, highly concurrent applications.

Do you want to know how to develop your skillset to become a Java Rockstar?
Subscribe to our newsletter to start Rocking right now!
To get you started we give you our best selling eBooks for FREE!
1. JPA Mini Book
2. JVM Troubleshooting Guide
3. JUnit Tutorial for Unit Testing
4. Java Annotations Tutorial
5. Java Interview Questions
6. Spring Interview Questions
7. Android UI Design
and many more ....
I agree to the Terms and Privacy Policy

Thank you!

We will contact you soon.

👁 Photo of Eleftheria Drosopoulou
Eleftheria Drosopoulou
November 20th, 2025Last Updated: November 14th, 2025
0 1,060 7 minutes read

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button
Close
wpDiscuz