background image

New Java Feature Scoped Values

by pp
Updated:

What are Scoped Values?

Scoped values enable you to transmit values to virtual threads, ensuring accessibility throughout the entire task execution. This proves particularly beneficial in scenarios requiring contextual information, such as HTTP Request Context, especially when your business logic encompasses nested method calls. Without ScopedValue implementation in virtual threads, your sole recourse would involve passing the context value through every method signature, eventually resulting in cluttered code. Scoped Values are compatible with conventional Java threads as well.

Create new project

First, we create project where we demonstrate ScopedValue feature.

mvn archetype:generate -DgroupId=com.lambdacoder -DartifactId=java22-scoped-values -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

add

  <properties>
    <maven.compiler.source>22</maven.compiler.source>
    <maven.compiler.target>22</maven.compiler.target>
  </properties>

to pom.xml

and because ScopedValue is still preview feature, we need to add also following part

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <release>22</release>
          <compilerArgs>--enable-preview</compilerArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>

to pom.xml.

the final pom.xml will contain:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.lambdacoder</groupId>
  <artifactId>java22-scoped-values</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>java22-scoped-values</name>
  <url>http://maven.apache.org</url>
  <properties>
    <maven.compiler.source>22</maven.compiler.source>
    <maven.compiler.target>22</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
          <release>22</release>
          <compilerArgs>--enable-preview</compilerArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Basic use of ScopedValue

Let's have a look at constructing and passing ScopedValues to tasks, observing how they remain accessible even within nested method invocations like execute().

In the provided Java code snippet, we have a demonstration of ScopedValues in action. The ScopedValuesDemo class encapsulates a Task inner class, which serves as the unit of work. Inside the Task class, a static TASK_ID instance of ScopedValue<Integer> is defined, serving as an example of Task Context.

When a task is executed, as depicted in the execute() method, it prints out its corresponding task ID.

package com.lambdacoder;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class ScopedValuesDemo {
    public static class Task {
        final static ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();

        public void execute() {
            System.out.println(STR."Running task in virtual thread with id: \{TASK_ID.get()}");
        }
    }

    public static void main(String[] args) {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 5).forEach(taskId -> executor.submit(() -> {
                ScopedValue.runWhere(Task.TASK_ID, taskId, () -> new Task().execute());
            }));
        }
    }
}

Once you compiled the project with mvn package, you can run the example. Note --enable-preview because ScopedValue hasn't made it to official release yet.

$ java --enable-preview -cp ./target/java22-scoped-values-1.0-SNAPSHOT.jar com.lambdacoder.ScopedValuesDemo
Running task in virtual thread with id: 2
Running task in virtual thread with id: 3
Running task in virtual thread with id: 0
Running task in virtual thread with id: 1
Running task in virtual thread with id: 4

Advanced use of ScopedValue with task nesting

Things get trickier, when you start new virtual threads. ScopedValue doesn't get propagated automatically when you submit new task to Virtual Thread Task Executor. Your options are either to create new ScopedValue with ScopedValue.runWhere(<<NEW SCOPED VALUE>>, <<TASK LAMBDA>>) or with scope.fork(<<TASK LAMBDA>>).

package com.lambdacoder;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.StructuredTaskScope;
import java.util.stream.IntStream;

public class ScopedValuesDemoWithSubtask {
    private static final ExecutorService EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
    final static ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();

    public static class Task {
        public void execute() {
            Integer id = TASK_ID.get();
            if (id == 100) {
                // submit task without scope
                EXECUTOR.submit(() -> new SubTask().execute());
            } else if (id == 101) {
                // submit new virtual thread which submits task with new scoped value
                EXECUTOR.submit(() ->
                        ScopedValue.runWhere(TASK_ID, 201, () -> new SubTask().execute()));
            } else if (id == 102) {
                // submit new scoped value where new virtual thread is launched - the task loses the scoped value
                ScopedValue.runWhere(TASK_ID, 202,
                        () -> EXECUTOR.submit(() -> new SubTask().execute()));
            } else if (id == 103) {
                // fork the same scoped value and submit new virtual thread, do that twice actually
                try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
                    scope.fork(() -> new SubTask().execute());
                    scope.fork(() -> new SubTask().execute());

                    scope.join();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(STR."Running Task in virtual thread with id: \{id }");
        }
    }

    public static class SubTask {
        public Integer execute() {
            // when scoped value is not available, print 999999 instead
            Integer id = TASK_ID.orElse(999999);
            System.out.println(STR."Running SubTask in virtual thread with id: \{id }");
            return 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntStream.range(100, 105).forEach(taskId -> EXECUTOR.submit(() -> {
            ScopedValue.runWhere(TASK_ID, taskId, () -> new Task().execute());
        }));

        Thread.sleep(100L);
        EXECUTOR.close();
    }
}

and check outcome. Note, that 999999 means there is no scoped value available:

$ java --enable-preview -cp ./target/java22-scoped-values-1.0-SNAPSHOT.jar com.lambdacoder.ScopedValuesDemoWithSubtask
Running SubTask in virtual thread with id: 999999
Running Task in virtual thread with id: 100
Running Task in virtual thread with id: 102
Running SubTask in virtual thread with id: 999999
Running Task in virtual thread with id: 104
Running SubTask in virtual thread with id: 201
Running Task in virtual thread with id: 101
Running SubTask in virtual thread with id: 103
Running SubTask in virtual thread with id: 103
Running Task in virtual thread with id: 103

Conclusion

In conclusion, this tutorial has provided a comprehensive overview of Scoped Values in Java, from their basic usage to advanced applications with task nesting. We've demonstrated how Scoped Values enable the propagation of contextual information across virtual threads, enhancing the clarity and efficiency of concurrent programming. By mastering Scoped Values, you can effectively manage task-specific data and optimize the performance of your multithreaded applications. We encourage you to experiment further with Scoped Values and explore their potential in your own projects. Happy coding!