Lesson 22: Scoped Values (ScopedValue) – Java 21+

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

FeatureThreadLocal (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 CaseLong-lived thread data (e.g., caches)Temporary context (e.g., user requests)

Lesson Reflection

  1. How does ScopedValue prevent memory leaks compared to ThreadLocal?
  2. Why is it safer to use immutable data in a multi-threaded environment?
  3. 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?

  1. 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.
  2. executor.submit(() -> processTask())
    • Each task runs inside the same scope and safely reads USER_ID.
  3. 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?

FeatureThreadLocal (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 ForLong-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) 😊🚀

Java Sleep