
Java 21 introduced Scoped Values, a new way to share data across threads safely without using ThreadLocal
. This feature is useful for passing contextual information without risking memory leaks or unwanted modifications.
1. What Are Scoped Values?
✅ ScopedValue
allows immutable data to be shared with child threads.
✅ It is safer and faster than ThreadLocal
because it prevents unintended modifications.
✅ Ideal for passing contextual data like user IDs, transaction info, or request metadata.
2. Before Java 21: Using ThreadLocal
(Not Always Safe)
🔴 Problem with ThreadLocal
:
- Memory leaks if not cleared properly.
- Unintended modifications (values are mutable).
- More overhead because values persist even after the thread is done.
📌 Example: Using ThreadLocal
(Before Java 21)
public class ThreadLocalExample {
private static final ThreadLocal<String> userContext = new ThreadLocal<>();
public static void main(String[] args) {
userContext.set("Alice"); // ❌ Must manually remove after use
new Thread(() -> System.out.println("User: " + userContext.get())).start();
userContext.remove(); // ❌ If forgotten, it can cause memory leaks
}
}
🚨 Problems:
❌ If remove()
is forgotten, it may cause memory leaks.
❌ Mutable data can lead to race conditions.
3. Java 21+ Solution: Scoped Values (ScopedValue
)
🟢 Scoped Values are immutable and automatically removed when the task is done!
📌 Example: Using ScopedValue
in Java 21
import java.lang.ScopedValue;
import java.lang.ScopedValue.Carrier;
public class Main {
private static final ScopedValue<String> USER = ScopedValue.newInstance(); // ✅ Immutable Context
public static void main(String[] args) {
ScopedValue.where(USER, "Alice").run(() -> {
System.out.println("User: " + USER.get()); // ✅ "Alice" is safely passed
});
System.out.println("Outside scope: " + USER.get()); // ❌ ERROR: No value outside scope
}
}
✅ Output:
User: Alice
Exception in thread "main" java.lang.IllegalStateException: No value present
✔ Scoped Values exist only inside their scope, preventing memory leaks!
4. When Should You Use Scoped Values?
🚀 Use Scoped Values when:
✔ You need to pass read-only data to child threads.
✔ You want to avoid memory leaks and unintended modifications.
✔ You are handling user sessions, request tracing, or logging metadata.
🚨 Do NOT use if:
❌ You need mutable shared data (Use a ConcurrentHashMap
instead).
❌ You need values to persist after a thread completes (Use a database or cache).
5. Comparison: ThreadLocal
vs. ScopedValue
Feature | ThreadLocal (Before Java 21) | ScopedValue (Java 21+) |
---|---|---|
Data Mutability | ❌ Mutable (Risk of unintended changes) | ✅ Immutable (Safer) |
Memory Leaks | ❌ Possible if remove() is not called | ✅ No leaks (auto-cleanup) |
Performance | ❌ More overhead (stores values for thread lifetime) | ✅ Faster (only exists in scope) |
Use Case | Long-lived thread data (e.g., caches) | Temporary context (e.g., user requests) |
Lesson Reflection
- How does
ScopedValue
prevent memory leaks compared toThreadLocal
? - Why is it safer to use immutable data in a multi-threaded environment?
- Can you think of a real-world scenario where
ScopedValue
would be useful?
Passing Scoped Values to Multiple Threads in Java 21+
ScopedValue
allows safe and immutable data sharing across multiple threads. Unlike ThreadLocal
, it automatically cleans up after execution, making it faster and safer for concurrent programming.
📌 Example: Passing a Scoped Value to Multiple Threads
Scenario:
We want to pass a user ID (ScopedValue<String>
) to multiple threads processing different tasks.
import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance(); // ✅ Immutable Scoped Value
public static void main(String[] args) {
try (ExecutorService executor = Executors.newFixedThreadPool(3)) { // Pool with 3 threads
ScopedValue.where(USER_ID, "Alice").run(() -> { // ✅ Scope starts here
for (int i = 1; i <= 3; i++) {
executor.submit(() -> processTask());
}
});
} // Auto-close ExecutorService
}
private static void processTask() {
System.out.println(Thread.currentThread().getName() + " processing for User: " + USER_ID.get());
}
}
✅ Output (Thread-safe execution with Scoped Value)
pool-1-thread-1 processing for User: Alice
pool-1-thread-2 processing for User: Alice
pool-1-thread-3 processing for User: Alice
✔ Each thread gets the correct USER_ID
without race conditions.
✔ The value is safely passed to multiple threads without risk of leaks.
✔ Threads cannot modify USER_ID
, ensuring data consistency.
🔹 How Does This Work?
ScopedValue.where(USER_ID, "Alice").run(() -> { ... })
- Creates a temporary scope where
USER_ID
is"Alice"
. - The value is available to all child threads created inside this block.
- Creates a temporary scope where
executor.submit(() -> processTask())
- Each task runs inside the same scope and safely reads
USER_ID
.
- Each task runs inside the same scope and safely reads
- Automatic Cleanup
USER_ID
is only accessible within the scope (avoids memory leaks).- Outside the scope, accessing
USER_ID.get()
throws an error.
📌 Example: Handling Multiple Users in Parallel
If we want to process multiple users at the same time, we can run separate ScopedValue blocks in parallel.
import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
public static void main(String[] args) {
try (ExecutorService executor = Executors.newFixedThreadPool(3)) {
Runnable task1 = () -> ScopedValue.where(USER_ID, "Alice").run(() -> processTask());
Runnable task2 = () -> ScopedValue.where(USER_ID, "Bob").run(() -> processTask());
Runnable task3 = () -> ScopedValue.where(USER_ID, "Charlie").run(() -> processTask());
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
}
}
private static void processTask() {
System.out.println(Thread.currentThread().getName() + " processing for User: " + USER_ID.get());
}
}
✅ Output (Each user runs in its own scope)
pool-1-thread-1 processing for User: Alice
pool-1-thread-2 processing for User: Bob
pool-1-thread-3 processing for User: Charlie
✔ Each user runs in an independent scope with no data conflicts!
🚀 Why Is This Better Than ThreadLocal
?
Feature | ThreadLocal (Before Java 21) | ScopedValue (Java 21+) |
---|---|---|
Data Mutability | ❌ Mutable (Risk of race conditions) | ✅ Immutable (Thread-safe) |
Memory Leaks | ❌ Possible (If remove() is forgotten) | ✅ No leaks (Auto-cleanup) |
Performance | ❌ High overhead (persists for thread lifetime) | ✅ Lightweight (Exists only in scope) |
Best For | Long-lived data (e.g., caches) | Short-lived context (e.g., request handling) |
🔍 Key Takeaways
✔ Scoped Values allow safe, immutable data sharing between threads.
✔ Unlike ThreadLocal
, Scoped Values do not leak memory and are automatically cleaned up.
✔ Best for passing user sessions, logging context, request metadata, etc., across threads.
The next Java 21+ feature: String Templates (STR.
for inline formatting) 😊🚀