Core Java

Java 21 Scoped Values

In Java programming, the essential task of sharing data among various components of an application running within a single thread has traditionally been addressed using ThreadLocal variables. However, given the emergence of VirtualThread and the increasing demand for scalability, the efficiency of sharing data through ThreadLocal variables is not always optimal. This is where Scoped Values come into the picture. Let us delve into understanding the Java 21 Scoped Variables.

1. Introduction

ThreadLocal is a class that allows you to create variables that can be accessed and mutated only by the same thread. Each thread that accesses a ThreadLocal variable gets its own, independently initialized copy. This means that changes made by one thread do not affect the values seen by other threads. To use ThreadLocal, you typically create an instance of it and then use its get and set methods to access and modify the variable, respectively. Here’s a simple example:

public class ExampleThreadLocal {
  private static final ThreadLocal < String > threadLocalVariable = new ThreadLocal < > ();

  public static void main(String[] args) {
    // Set the thread-local variable
    threadLocalVariable.set("Hello, ThreadLocal!");

    // Access the thread-local variable
    String value = threadLocalVariable.get();
    System.out.println(value);
  }
}

ThreadLocal is particularly useful in scenarios where data needs to be associated with a thread, and sharing that data between threads is not required. Common use cases include managing per-thread resources, such as database connections or user context in a web application. While ThreadLocal can be a powerful tool, it’s essential to use it judiciously. Improper usage can lead to memory leaks and difficulties in debugging. Be sure to clear the ThreadLocal variables appropriately, especially in scenarios where threads are reused or pooled.

2. Understanding the Challenges with ThreadLocal

ThreadLocal is a class in Java that provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

  • Limited to a Single Thread: One of the primary challenges with ThreadLocal is that it is limited to a single thread. While this is its intended behavior, it can become a challenge in scenarios where sharing data between multiple threads is necessary. This limitation can lead to increased complexity in certain multi-threaded applications.
  • Potential for Memory Leaks: Incorrect usage of ThreadLocal can result in memory leaks. If a thread-local variable is not appropriately cleared after its use, it prevents the associated thread from being garbage collected, leading to unnecessary memory consumption. Developers need to be mindful of cleaning up ThreadLocal variables to avoid such issues.
  • Global State in Disguise: While ThreadLocal variables are local to a thread, excessive use of them can introduce a form of global state. This is because multiple threads have their copies of thread-local variables, but they are all operating on a shared codebase. This shared codebase can introduce hidden dependencies and make the code harder to reason about.
  • Difficulty in Testing: Testing threaded code that relies heavily on ThreadLocal variables can be challenging. The behavior of these variables depends on the specific execution context of each thread, making it harder to predict and control in unit tests. Developers need to employ advanced testing techniques or mocks to effectively test code utilizing ThreadLocal.

3. Java 21 Scoped Variables

Java 20 introduced a notable feature known as scoped values (JEP 429). This feature facilitates the sharing of immutable data within a thread and across threads without relying on method arguments. While thread-local variables offer similar functionality, scoped values introduce distinct concepts that make them a more preferable choice. Scoped values are specifically designed to enable lightweight data sharing. The introduction of virtual threads has shed light on the limitations of thread-local variables. These variables often prove too intricate and resource-intensive for efficient data sharing. Consider the scenario where an application employs millions of virtual threads, each equipped with mutable thread-local variables – this situation could potentially lead to memory leaks. To address this concern, Java introduced scoped values. These values are immutable, possess a bounded lifetime, and provide control over how data is inherited by child threads.

3.1 Working with Scoped Values in Java

Scoped values offer a powerful way to share data within a thread while adhering to the immutability concept. Let’s explore how to use scoped values with an example using the following class to store employee data:

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class Employee {
  private int id;
  private String name;
}

Scoped values are created using the ScopedValue class, which has a parameterized type allowing you to define the type of the variable to be shared. To illustrate, let’s define a scoped value for our Employee class:

public static final ScopedValue CURRENT_EMPLOYEE = ScopedValue.newInstance();

To bind scoped values, you can use the where method, which comes in different variations. The first one returns a Carrier and allows you to bind multiple values by chaining multiple .where() calls. You can then execute a method using run (for Runnable) or call (for Callable). The scoped values will be accessible in the invoked method as well as other methods called indirectly, as long as it’s in the same thread.

ScopedValue
  .where(CURRENT_EMPLOYEE, employee)
  .where(REQUEST_TIME, ZonedDateTime.now())
  .run(employee::handleRequest);

The second and third methods directly accept and execute a Callable or Runnable. They essentially call it the first method. However, it’s important to note that the second and third methods do not support multiple-scoped values.

After the execution of the run or call method finishes, the binding is destroyed. This ensures that data is transmitted only from the caller to the callee. The callee cannot modify the data from the caller. Nonetheless, the callee can create another binding when calling another method. Additionally, the lifetime of scoped variables is confined within the run or call method executed by the Carrier.

3.2 Understanding Immutability in Scoped Values

One key characteristic of scoped values is their immutability. Once a value is set using the ScopedValue class, it cannot be modified. However, it is possible to rebind values, introducing a useful flexibility. Consider the scenario where the LoggerService requires logging names in uppercase. In such cases, you can create a new instance of the Employee class with an uppercase name.

To rebind the values and ensure that the LoggerService receives the employee with an uppercase name, invoke ScopedValue.where again to create a new Carrier. Provide the new Employee instance as the second argument. By doing this, attempting to retrieve the value from the ScopedValue variable within the method executed by the new Carrier will yield the rebound value.

// Rebinding the values for uppercase logging
ScopedValue
  .where(CURRENT_EMPLOYEE, newEmployeeInstanceWithUpperCaseName)
  .run(loggerService::logUpperCaseName);

3.3 Optimal Data Sharing with Records in Thread Context

When the need arises to share multiple values within the thread context and among child threads, a recommended approach is to create a record that encapsulates all the required values. This record can then be shared through conventional means.

One significant advantage of adopting this approach is that, in the future, if there is a requirement to add more values or remove certain values from the scope, the necessary code changes will be minimal, resulting in a more maintainable codebase.

// Define an ApplicationContext record
public record ApplicationContext(Principal principal, Role role, Region region) {}

// Create an instance of ApplicationContext
private final ApplicationContext CONTEXT = new ApplicationContext(...);

// Use ScopedValue to run a block of code with the shared context
ScopedValue.runWhere(ApplicationContext, CONTEXT, () -> {
  doSomething();
});

4. Scoped Values vs Thread-Local Variables

  • Immutability: Scoped values are immutable, as the ScopedValue class lacks any setter method. This immutability results in a performance advantage, making it as fast as reading a local variable. In contrast, thread-local variables are mutable, allowing data to flow in any direction due to the presence of a set method. This can sometimes lead to challenges in understanding the data flow.
  • Lifetime: Scoped values are available for a bounded period, confined to the Callable or Runnable invoked by the Carrier. After execution, all bindings are destroyed. Thread-local variables, however, have an unbounded lifetime. Once set using the set method, the value persists as long as the thread is alive, although removal using the remove method is possible but not always practiced.
  • Inheritance: Scoped values are not inherited to child threads by default. However, passing values to child threads can be achieved using StructuredTaskScope. This allows child threads to reuse bindings from the parent thread without copying, resulting in more cost-effective inheritances with less overhead.

5. Conclusion

In conclusion, our exploration of Java programming has encompassed key concepts such as ThreadLocal, Scoped Values, and additional features. ThreadLocal, a mechanism for thread-local variables, offers isolated copies for each thread, though challenges like limited scope and potential memory leaks should be considered. Java 20 introduced Scoped Values, providing an alternative for lightweight data sharing within a thread. Immutable and designed for simplicity, Scoped Values address challenges associated with thread-local variables, particularly in the context of virtual threads and scalability. The practical use of Scoped Values involves creating instances with various data types and binding them using the ScopedValue.where method. Immutability ensures data transmission without modification, while the ability to rebind values provides flexibility for data transformation. For optimal data sharing, leveraging records in Java is recommended. Records offer a concise way to encapsulate and share data, enhancing maintainability by minimizing code changes when adding or removing values.

Yatin

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

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

0 Comments
Inline Feedbacks
View all comments
Back to top button