BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Enhancing Java Concurrency with Scoped Values in JDK 21 (Preview)

Enhancing Java Concurrency with Scoped Values in JDK 21 (Preview)

Scoped Values is now in JDK 21 as a Preview Feature. Alongside Virtual Threads and Structured Concurrency, Scoped Values add to the growing list of enhancements to Java and Project Loom.

Scoped values can be accessed from anywhere, providing that a dynamic scope has been created and the desired value bound into the scope. Imagine a call chain of methods with a faraway method that needs to use data. The data would need to be passed down the call chain with the caution that it might be changed by any method till the callee is reached.

A Scoped value behaves like an additional parameter for every method in the sequence of calls, but none of the methods actually declare this parameter. Only the methods that have access to the ScopedValue object can retrieve its value, which represents the data being passed. As stated in JEP 446, Scoped Values (Preview)

Scoped Values improve safety, immutability, encapsulation, and efficient access within and across threads

Applications that use transactions, security principals, and other forms of shared context in a multithreaded environment will be able to benefit from them. However, they are not intended to replace the ThreadLocal variables introduced in Java 1.2.

The difference between the two is the choice of mutability and, in some cases, safety. While thread-local allows for values to be set and changed, Scoped values take a different approach by controlling shared data, making it immutable and bound for the lifetime of the scope.

A ThreadLocal variable is a global variable, usually declared as a static field, that has accessor methods. This makes the variables mutable, as the setter can change the value. With every new thread, you get the value already present in the spawning thread, but it can be changed in the new thread without affecting the value in the thread that spawned it.

However, it also poses some challenges, such as the ThreadLocal variable being a global mutable. This can result in tracing and debugging challenges in some cases, as the thread-local can be modified a long way from where it is created (sometimes referred to as "spooky action at a distance", a reference to Einstein’s remark about quantum mechanics). A further, more minor issue is that they cause a larger memory footprint as they maintain copies for each thread.

Scoped Values, on the other hand, introduce a different way to share information between components of an application by limiting how the data is shared, ensuring it is immutable and has a clear and well-defined lifetime. A scoped value is created using the factory method newInstance() on the ScopedValue class, and a value is given to a scoped value within the context of a Runnable, Callable or Supplier calls. The following class illustrates an example with Runnable:

 

public class WithUserSession {
	// Creates a new ScopedValue
	private final static ScopedValue<String> USER_ID = new ScopedValue.newInstance();

	public void processWithUser(String sessionUserId) {
		// sessionUserId is bound to the ScopedValue USER_ID for the execution of the 
		// runWhere method, the runWhere method invokes the processRequest method.
		ScopedValue.runWhere(USER_ID, sessionUserId, () -> processRequest());
	 }
	 // ...
}

In the above class, the first line creates a scoped value called USER_ID, and the method processWithUser(String sessionUserId) invokes the processRequest() method with the scope via the runWhere() method, thereby executing the run method to handle the request. The value is valid within this method and anywhere else invoked within the method. The lifespan of the scoped value is well-bounded, eliminating the risk of memory or information leaks.

 

There is no set() method in ScopedValue. This ensures the value is immutable and read-only for the thread. However, it also allows for cases where the caller requires the result after the callee has finished processing. For example, in the callWhere() method, a returning-value operation will bind a value to the current Thread. In the runWhere example method above, the processRequest() method was called, but no returning value was expected. In the following example, the value returned from the squared() method will be returned and stored in the multiplied variable. callWhere() uses the Callabale<V>, whereas the runWhere() method expects a Runnable interface.

public class Multiplier {
	// Creates a new ScopedValue
	ScopedValue<BigInteger> MULTIPLIER = ScopedValue.newInstance();

	public void multiply(BigInteger number) {
		// invokes the squared method and saves the result in variable multiplied
		var multiplied = ScopedValue.callWhere(MULTIPLIER, number, () -> squared());
	}
	// …
}

A Scoped value is bound to a value on the current thread. However, rebinding is possible for an execution of a new method. The rebinding is not allowed during the execution of the scope, however, once the scoped execution is completed, a rebinding can done. This is different from a ThreadLocal, where binding can be done anytime during the execution by using a setter method.

Furthermore, to read a scoped value from the thread, simply call the get() method. However, calling get() on an unbound scoped value throws a NoSuchElementException. If unsure, check if the scoped value is bound using ScopedValue.isBound(). There are also two methods, orElse(), and orElseThrow(), to provide a default value or exception, respectively.

One critical distinction between thread-local variables and Scoped values is that the latter is not bound to a particular thread. They are only set for a dynamic scope - such as a method call issued, meaning a single scoped value cannot have different values in the same thread.

In other words, it's useful for a "one-way transmission" of data. A ThreadLocal has an unbounded lifetime and does not control the changing of data throughout that lifetime. Moreover, it is an expensive operation, with the values being copied to each child thread. With a scoped value, it is set once and can then be shared over multiple threads, as shown in the example below, where three forks of the Task share the same variable number.

        ScopedValue.runWhere(MULTIPLIER, number, () -> {
            try (var scope = new StructuredTaskScope<BigInteger>()) {

                scope.fork(() -> squaredByGet());
                scope.fork(() -> squaredByGet());
                scope.fork(() -> squaredByGet());

            }
        });

While sharing values between threads in this way is beneficial, the cache sizes for Scoped values are limited to 16 entries. To change the default size, one can use the following parameters while invoking the Java program.

java.lang.ScopedValue.cacheSize

The introduction of Scoped Values aims to solve the limitations associated with ThreadLocal variables, especially in the context of virtual threads. Although it's not absolutely necessary to move away from ThreadLocal, Scoped Values significantly enhances the Java programming model by providing a more efficient and secure way to share sensitive information between components of an application. Developers can learn more details on Scoped Values in the JEP-446 documentation.

We may expect significant numbers of the current use cases of thread-local variables to be replaced by scoped values over time - but please note that Java 21 unfortunately only brings Scoped Values as a Preview Feature - we will have to wait a bit longer before the feature arrives as final.

 

About the Author

Rate this Article

Adoption
Style

BT