Java 20 has been released in mid of March and Project Loom brought us Scoped Values as a new incubator feature with it, which is supplementing the previously released Virtual-Threads and Structured Concurrency preview features.
Scoped Values are meant to replace and enhance the ancient ThreadLocal variables, introduced in Java 1.2 about 25 years ago. ThreadLocal variables are a way to share a variable between methods and classes executed on the same thread. This is a feature not often used by regular developers, but extremely helpful for the folks developing frameworks and toolkits. It removes the need to pass a variable to each method if the value is needed extensively by the framework.
A well known use case for ThreadLocal variables is for example the database transaction management for a single request in a web application. If your application writes something to your database you will need to manage your transaction during the processing of your request. If something goes wrong during processing you rollback the transaction and if all work is done correctly, you commit the transaction to the database. To implement this, ideally all processing methods have access to the transaction object to act on it accordingly. If you declare your transaction at the root of your application as a ThreadLocal variable, all child method invocations will have access to the transaction by the ThreadLocal object without having to declare it in every method call as long as the request is processed by a single thread.
However, the use of ThreadLocal variables comes with some major disadvantages. Since the variable is mutable, you can change it from any method on the current thread, which might lead to hard to find bugs. Another drawback is the fact that threads rarely get garbage collected in web applications and by this the ThreadLocal variables bound to it will also almost permanently live on the heap. This might increase your memory consumption quite a bit. To make things worse on the memory front, if you create child thread from the current thread, all ThreadLocal variables will be copied to the child thread as well, even if the child thread won’t need them. If you have several layers of nested threads, this can quickly become a memory problem since all threads will have their own copy of all ThreadLoacal variables from all parents.
Scoped Values: The better ThreadLocals
Since the multithreading code in Java has been refactored by Project Loom for the upcoming full release of Virtual Threads in Java 21, it was also a good time to provide a better mechanism for the ThreadLocal mechanics, which resulted in JEP 429 and Scoped Values.
Scoped Values fix the above mentioned two disadvantages of the ThreadLocal class but otherwise give the developer a very similar programming model. Scoped Values can be used to share data within a Thread but are immutable, which is sufficient for most use cases. If you create a child thread from the current one, the child thread will also inherit the Scoped Values, but since they are immutable, this will only be a reference to the instance of the variable and not a copy.
Let’s start with a simple example to see a Scoped Value in action:
import jdk.incubator.concurrent.ScopedValue;
public class ScopedValuesSimple {
public final static ScopedValue<String> USERNAME = ScopedValue.newInstance();
private static Runnable printUsername
= () -> System.out.println("Username is "+USERNAME.get());
public static void scopedValueInSimpleAction() throws Exception {
ScopedValue.where(USERNAME, "Bob").run(printUsername );
ScopedValue.where(USERNAME, "Chris").run(printUsername );
System.out.println("Username bound: "+ USERNAME.isBound());
}
public static void main(String[] args) throws Exception {
scopedValueInSimpleAction();
}
}In line (1), I import the the ScopedValue type, which is the main Java class you need to use for the new features In line (5), I declare a variable USERNAME of type ScopedValue. You can see that ScopedValue takes a generic, which specifies the type of the scoped variable you want to use it for. To actually create it, ScopedValue has a static factory method newInstance(). Notice that the ScopedValue variable is public and static and can hence be accessed from anywhere in the code. The actual value of the scoped variable is not yet specified. We will come to that in a bit.
In line (7) and (8) I declare an anonymous Runnable that just uses the USERNAME variable and print its value to the console. ScopedValues have a get() method that returns the bound value of the variable in the current scope. Since I declared the value as String in line (5), the get method also returns a String. If there is no value bound to the variable the get call would result in an exception thrown by the method.
In line (12) and (14) it becomes interesting, since I declare two values for the USERNAME variable by two scopes to use it in. You declare the value for the scope using the static where() method, which takes a ScopeValue type and the value it should be bound to. The type of the value has to match the generic used in line (5). To start a new scope, use the run() method and pass in the Runnable. In both cases I am using the runnable declared in line (7)-(8) to print the name to the console. In line (16), I check if my USERNAME variable has a value bound to it by the isBound() method. Since I use it outside of the two scopes declared previously, this will return false.
Running the program provides the following output:
Username is Bob
Username is Chris
Username bound: falseWe can see that the username is different for the two invocations of the Runnable that prints the username, since they were started in two different scopes in line (12) and (14). We can also see that the variable is not bound to any value outside the two scopes, which prints false in line (3).
On thing to note is, that although I pass a Runnables to the ScopeValue to start a new scope, no new thread is created to run it. The Runnable runs on the current thread and also blocks until it is finished.
Let’s expand our example to actually start some virtual threads in a scope.
For this, we first need a Callable that will be worked on by our virtual threads. I will just reverse the user name in the current scope as an example for a task that is run:
private static Callable<String> revertUsername = () -> {
if (USERNAME.isBound()) {
return new StringBuilder(USERNAME.get()).reverse().toString();
};
return "";
};Next, we need a bunch of virtual threads which will execute the above Callable. Here is some code that starts the Callable three times on different virtual threads:
private static class VirtualThreadStarter implements Runnable {
public void run() {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> task1 = scope.fork(revertUsername);
Future<String> task2 = scope.fork(revertUsername);
Future<String> task3 = scope.fork(revertUsername);
try {
scope.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(task1.resultNow());
System.out.println(task2.resultNow());
System.out.println(task3.resultNow());
}
}
}The code opens a new StructuredTaskScope in line (3) and starts three virtual threads in it with our reverse Callable to execute. It then waits for the Callables to complete and prints the reversed name to the console. If you are not familiar with the new structured concurrency API, check out this post.
To glue everything together, we need a couple of usernames to work on. In this code fragment, I loop over a list of Strings, start a new scope for each name and use the VirtualThreadStarter to start threads for it:
public static void main(String[] args) throws Exception {
List<String> names = List.of("Ernie", "Bert", "Jack");
for (String username : names) {
ScopedValue.where(USERNAME, username).run(new VirtualThreadStarter());
}
}Running this code will print each reversed user name three times to the console:
einrE
einrE
einrE
treB
treB
treB
kcaJ
kcaJ
kcaJWe can see that the threads run in three different scopes, one for each name and that the value of the scope was inherited from the scope they were started in.
Of course this example is very simple, but the subthreads could actually execute a task that can be run in parallel and required the current username for security reasons.
As always, you can find the source code for these examples on Github.
Schreibe einen Kommentar