Thursday, September 28, 2023

Java: Scoped Values

7 min read
(Updated: Thursday, September 28, 2023)

1. Introduction

Scoped Values provides a secure way of sharing data within and across threads, that was introduced as part of JEP 429, to solve a common challenge in concurrent programming. Scoped Values are Java's approach to providing a simple, immutable, and inheritable mechanism for data sharing. In the realm of multithreaded applications, ensuring thread safety and data consistency is paramount. Scoped Values address this by allowing data to be shared in a controlled manner.

Key Characteristics

  • Immutability: Once a scoped value is created, it cannot be modified. This immutability is crucial for avoiding concurrency-related issues like race conditions.
  • Inheritance: Scoped values can be inherited by child threads, ensuring consistency across thread hierarchies.
  • Simplicity: They offer a more straightforward alternative to traditional concurrency mechanisms like thread-local variables.

2. Why Scoped Values?

In the Java programming language, data is usually passed to a method by means of a method parameter, where it may need to be passed through a sequence of many methods to get to the method that makes use of the data. That is to say, every method in the sequence of calls needs to declare the parameter and every method has access to the data.

ScopedValue provides a means to pass data to a faraway method (typically a callback) without using method parameters. It is "as if" every method in a sequence of calls has an additional parameter. None of the methods declare the parameter and only the methods that have access to the ScopedValue object can access the value.

3. Scoped Values

3.1 See in action

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

    ScopedValue.runWhere(NAME, "duke", () -> doSomething());
    ScopedValue.runWhere(NAME, "duke2", () -> doSomething());

From the example, ScopedValue makes it possible to securely pass data from caller (this function) to a faraway callee (doSomething) through a sequence of intermediate methods that do not declare a parameter for the data and have no access to the data.

3.2 Rebinding

Scoped values are immutable. In some more complex use cases, where different contextual data is required under corresponding scopes, "rebinding" becomes a critical feature, to allows a new binding to be established for nested dynamic scopes. In the above example, suppose that code executed by doSomething binds NAME to a new value with: ScopedValue.runWhere(NAME, "duchess", () -> doMore()); Code executed directly or indirectly by doMore() that invokes NAME.get() will read the value "duchess". When doMore() completes, the original value will be available again (the value of NAME is "duke").

3.3 Inheritance

ScopedValue supports sharing across threads. This sharing is limited to structured cases where child threads are started and terminate within the bounded period of execution by a parent thread. When using a StructuredTaskScope, scoped value bindings are captured when creating a StructuredTaskScope and inherited by all threads started in that task scope with the fork method.

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

    ScopedValue.runWhere(NAME, "duke", () -> {
        try (var scope = new StructuredTaskScope<String>()) {
    
            scope.fork(() -> childTask1());
            scope.fork(() -> childTask2());
            scope.fork(() -> childTask3());
    
            ...
         }
    });

In this code example, the ScopedValue NAME is bound to the value "duke" for the execution of a runnable operation. The code in the run method creates a StructuredTaskScope that forks three tasks. Code executed directly or indirectly by these threads running childTask1(), childTask2(), and childTask3() that invokes NAME.get() will read the value "duke".

4. Prior to "Scoped Value"

In Java application that runs multiple components and layers in different threads, and that needs to share data within between, from earlier Java implementations, people usually use a few approaches:

4.1 ThreadLocal

ThreadLocal commonly used to maintain thread-specific data that is not shared between threads. Thread-local variables usually have a publicly available declaration that is able to access across components. For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.

Java
 public class ThreadId {
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID
     private static final ThreadLocal<Integer> threadId =
         new ThreadLocal<Integer>() {
             @Override Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };

     // Returns the current thread's unique ID
     public static int get() {
         return threadId.get();
     }
 }

These variables are unique to each thread that accesses them, where each thread has its own, independently initialized copy of the variable. Note that after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

A ScopedValue excels over a ThreadLocal in cases where the goal is "one-way transmission" of data without specifying method parameters. However, thread-safety and performance should be properly evaluated. For instance:

  • ThreadLocal does not prevent code in a faraway callee from setting a new value.
  • A ThreadLocal has an unbounded lifetime even after a method completes unless explicitly removed.
  • Inheritance is expensive - the map of thread-locals to values must be copied when creating each child thread.

4.2 InheritableThreadLocal

InheritableThreadLocal is to provide inheritance of values from parent thread to child thread. This is useful when someone want to pass data from a parent thread to a child thread.

Java
public class InheritableThreadLocalExample {
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        inheritableThreadLocal.set("Parent thread value");

        Thread childThread = new Thread(() -> System.out.println(inheritableThreadLocal.get()));
        childThread.start();
    }
}

Not exactly comparable to the ScopedValue due to the purposes and performance consideration.

4.3 Synchronization, Locks and thread-safe data structures

Synchronization and locks focusing on using synchronized blocks or explicit locks (like ReentrantLock) to control access to shared resources. Thread-safe data structures designed for concurrent access and modification between various threads. They are focusing on thread safety and concurrency control, but not offering thread-local storage.

5. ScopedValue in Other Languages

ScopedValue is fairly generic concept that is applicable in various programming languages and libraries. Scoped values are often used in languages like Python, C++, Go etc., with constructs like context managers and RAII (Resource Acquisition Is Initialization) to manage resources, such as file handles, database connections, or locks, safely and automatically.

5.1 Python

The contextvars module in Python 3.7+ offers a way to manage context-local state, similar to scoped values but with more focus on asynchronous tasks.

Python
from contextvars import ContextVar
import asyncio

# Define a context variable
ctx_var = ContextVar('example_var', default='Default Value')

async def main():
    # Set a new value for the context
    ctx_var.set('New Value')
    print(ctx_var.get())  # Outputs 'New Value'

    # Spawn a new task
    await asyncio.create_task(sub_task())

async def sub_task():
    # This will inherit the context of the parent task
    print(ctx_var.get())  # Outputs 'New Value'

asyncio.run(main())

5.2 Go

Go’s goroutines and channels provide a different approach to concurrency, focusing on message passing rather than shared state.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    messageChan := make(chan string)

    wg.Add(1)
    go func() {
        defer wg.Done()
        messageChan <- "Shared Message"
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()
        msg := <-messageChan
        fmt.Println(msg)
    }()

    wg.Wait()
}

6. Real-world Application Example

Use case: In an enterprise web service application, every incoming HTTP request is being processed in its own thread. Often, it's necessary to include context-specific information, like a unique requestId, to allow trace the flow of a particular request/response throughout the process.

One common practice is to pass along context through multiple layers and methods, that clutter the method signatures.

With ScopedValue, here's how it looks:

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

public void handleRequest(Request request) {
    // generateRequestId could generate a random UUID for example.
    String requestId = generateRequestId();
    ScopedValue.runWhere(REQUEST_ID, requestId, () -> processRequest(request));
}
Java
public class RequestProcessor {
    public void performTask() {
        String requestId = REQUEST_ID.get();
        log("Executing task for request: " + requestId);
        // Additional processing
    }
}

This example illustrates how ScopedValue can be a powerful tool for implicit, thread-safe data passing in complex, multi-layered Java applications, particularly for logging purposes where contextual information needs to be available across various components of the application.

Simplified Context Passing: ScopedValue eliminates the need for passing the request ID through each method call, leading to cleaner code. Thread Safety: Each thread has its own binding to the ScopedValue, ensuring correct and isolated logging context per request. Dynamic Scope: The value is available throughout the dynamic scope of the request handling, reverting back once the request processing is completed.

7. Conclusion

Scoped Values in Java provide a significant advancement in handling data in multithreaded environments, by offering a secure, simple, and consistent way to share immutable data across threads, enhancing both the safety and readability of concurrent Java applications. Happy coding!

You might want to check these out ↘

How Your Database Stands Up to the Ultimate Reliability Check: ACID rules

How Your Database Stands Up to the Ultimate Reliability Check: ACID rules

For IT professionals in database management, grasping the core principles that protect our digital transactions is essential. Today, we're exploring the ACID model, a pivotal framework for ensuring transaction reliability and security. This goes beyond mere theoretical knowledge; it's vital for the smooth operation of diverse systems, from financial processes to social media platforms. We'll delve into the ACID principles and their critical role in real-world applications.
Convert String to Date: The Java Ways

Convert String to Date: The Java Ways

Converting strings to dates in programming is a common task that's more complex than it seems. This challenge is rooted in the wide range of date formats and the specifics of time zones. Converting a String to a Date in Java can be done in several ways, depending on the Java version you're using and whether you are incorporating external libraries. Below are the primary methods used in different versions of Java and through some commonly used external libraries.
Cookbook: How to Effectively Revert a Git Commit

Cookbook: How to Effectively Revert a Git Commit

As a software engineer, encountering situations where recent changes need to be undone is not uncommon. It may be due to errors, or in some cases just need a significant rework. When such scenarios arise while using Git as your version control system, there’s a few approaches that can help revert those changes effectively. Let’s take a closer look.
Technology
Trending
Contact Us

Stay ahead of the curve
on software insights and technology trends.