Intro

This feature is a proposed solution for long lasting difficulties when we need to provide some values in a different part of logic. Let’s say we have a typical spring boot web application, when request is received by it web framework creates a dedicated thread, applies filters, creates objects that describe the request like user info, request and so on, triggers routing logic to decide which controller has to be used. Then controller, written by us, takes control and does some logic, for example extracts data from the database based on some parameters from the request. Usually we will use Spring data to interact with database, meaning control will be given to it. At this moment repository layer may need something from context application, for example authenticate user info. In Spring we can get it by injecting the right bean into our service from application context. typical web app flow

What if we would need to pass this object through a layer above - our controller and service, they have no use of this object, they just need to pass it as parameter into the methods or constructors of service downstream. This is still fine, but it can quickly become very clumsy with many such objects passed to downstream services. cumbersome web app flow

One of the solution to avoid it would be to use ThreadLocal or InheritedThreadLocal variables - variables that hold different value per thread. This would allow to avoid the need to alternate signature of each and every method in the chain which at the end requires some data from the caller.

public class Framework {

    private final Application application;

    public Framework(Application app) { this.application = app; }
    
    private final static ThreadLocal<FrameworkContext> CONTEXT 
                       = new ThreadLocal<>();

    void serve(Request request, Response response) {
        var context = createContext(request);
        CONTEXT.set(context);
        Application.handle(request, response);
    }

    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();
        var db = getDBConnection(context);
        db.readKey(key);
    }

}

Disadvantages of traditional approach:

But using thread-local variables brings some challenges as well:

  • thread-local variables are mutable, which means any part of our logic can update it for the entire thread. This may lead to unpredictable data flow
  • thread-local variables require additional attention to lifecycle management to prevent leaking values to different tasks or leaking memory by allocating space when it is not needed anymore
  • thread-local variables maybe be inherited by child threads, which comes with additional burden on memory because each child thread has to maintain it’s own copy of a variable
  • with virtual threads memory footprint issues may become even more severe, because with lightweight threads we now have much more thread-local copies

Scoped values solution

Definition

According to JEP 481:

A scoped value is a container object that allows a data value to be safely and efficiently shared by a method with its direct and indirect callees within the same thread, and with child threads, without resorting to method parameters. It is a variable of type ScopedValue. It is typically declared as a final static field, and its accessibility is set to private so that it cannot be directly accessed by code in other classes.

Scoped values visually or syntactically define the scope and lifecycle of a value that drastically simplifies understanding where value can be used.

Example

Take a look at the following example:

class Framework {

    private final static ScopedValue<FrameworkContext> CONTEXT
                        = ScopedValue.newInstance();    // (1)

    void serve(Request request, Response response) {
        var context = createContext(request);
        ScopedValue.runWhere(CONTEXT, context,          // (2)
                   () -> Application.handle(request, response));
    }
    
    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();                    // (3)
        var db = getDBConnection(context);
        db.readKey(key);
    }
}

Here at position (1) you can see scoped variable (a container) creation, at position (2) - binding of scoped variable to its value and that the same time defining in which logic this value will be accessible. In this case context value will be accessible in Application.handle(request, response) and all stack of methods inside it. By business logic handle eventually calls readKey method of a framework which can easily get access to CONTEXT scoped value (position 3). If scoped variable’s get method will be called outside of ScopedValue.runWhere an exception will be thrown that says that variable is not bound to a thread.

Benefits

The benefits of such approach is:

  • value is accessible in much more predictable scope
  • lifecycle is much more predictable as well
  • scoped variable is immutable and thereby provides only one direction from top to bottom data flow.

Regarding the last point, despite variable is immutable we can rebind it for internal invocations like this, so that we can still provide different versions of scoped value to downstream methods.

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

void foo() {
    ScopedValue.runWhere(X, "hello", () -> bar());
}

void bar() {
    System.out.println(X.get()); // prints hello
    ScopedValue.runWhere(X, "goodbye", () -> baz());
    System.out.println(X.get()); // prints hello
}

void baz() {
    System.out.println(X.get()); // prints goodbye
}

One last point addressed by scoped value is memory footprint. By using structured concurrency approach described in JEP 480 each child thread will not only properly inherit scoped value, but also properly organise scope value lifecycle. Unfortunately traditional ForkJoinPool does not guarantee this.

Additional API capabilities of ScopedValue

ScopedValue API also provides:

  • not only running piece of code without results, but also calling it to receive result back (or throw an exception)
    try {
      var result = ScopedValue.callWhere(X, "hello", () -> bar());
    catch (Exception e) {
      handleFailure(e);
    }
    
  • binding multiple scoped values at once
    ScopedValue.where(k1, v1).where(k2, v2).run(() -> ... );
    

Conclusion

ScopedValues are yet another great step forward to improve efficiency and convenience when dealing with multithreaded use cases. In combination with virtual thread and structured concurrency it provides a solid base to write complex multi threaded application in much more reliable way. You can find much more detailed information at the source info

Updated: