New Java Feature Scoped Values
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!