Contents

Scoped Values in Java

Written by: David Vlijmincx

Scoped values are a new way to share data between threads without using method parameters. They also use less memory compared to Thread local variables.

What is a Scoped Value

Scoped values are a new addition to the Java language. I was first introduced as an incubating feature in Java 20 as JEP 429, as a preview feature in Java 21 in JEP 446, and is currently in the second preview round in Java 22 as JEP 464 . Because Scoped values are still a preview feature you need to pass the --enable-preview parameter when running your application.

Scoped values allow you to share data between threads in a memory-efficient way. They are memory efficient because the data inside a scoped value is immutable. This means that many threads can use the data without every thread needing a copy of the data. This is especially useful in combination with virtual threads. When running lots of virtual threads you will only need a single copy of the data for any number of virtual threads.

With a scoped value you essentially create a scope that is bound to the current thread, and within this scope, the scoped value is accessible everywhere in the code running in that thread.

Scoped Value for a single task/ current thread

In the following example, I create a scope using ScopedValue.runWhere(). The runWhere() method takes three parameters:

  • Location of the scoped value (which one do you want to set)
  • The value of the scoped value
  • The runnable that runs within this scope.

For the first parameter, you need to create a scoped value inside a class like this: final static ScopedValue<String> VALUE = ScopedValue.newInstance();

In the following example, the Task.VALUE scoped value is set to testing. This value is only accessible by the thread running new Task().doStuff(). In this case that will be the main thread. When the runnable () -> new Task().doStuff() is done the scope of the scoped value is closed and no longer accessible.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class scopedValuesDemo {

    public static void main(String[] args) throws InterruptedException {
        ScopedValue.runWhere(Task.VALUE, "testing", () ->  new Task().doStuff());
    }
}

class Task  {

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

    public void doStuff() {
        System.out.println("VALUE = " + VALUE.get());
    }
}


The runWhere method will create a scope and inside that scope, the scoped value will be accessible to the runnable.

Scoped value with Virtual Thread

Using scoped values with Virtual threads is very straightforward. The following example is the same as the previous one, but now ScopedValue.runWhere(... is inside the creation of a virtual thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class scopedValuesDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread.startVirtualThread(() ->
                ScopedValue.runWhere(
                        Task.VALUE, "testing", () -> new Task().doStuff()
                ));

        Thread.sleep(100); // Giving the thread time to print to the console
    }
}

class Task  {

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

    public void doStuff() {
        System.out.println("VALUE = " + VALUE.get());
    }
}

On line 4 a virtual thread is created that starts a new scope.

Rebinding Scoped Value

You can also rebind scoped values by nesting scopes. This is useful if you want to change the value of a scoped value. Rebinding is done by nesting scopes that refer to the same scoped values. In the following example the doStuff() method is already inside a scoped value but created a new nested scoped value. Now Task().someOtherTask() runs inside a new thread and scoped value with a different value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class scopedValuesDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread.startVirtualThread(() ->
                ScopedValue.runWhere(
                        Task.VALUE, "testing", () -> new Task().doStuff()
                ));

        Thread.sleep(100);
    }
}

class Task  {

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

    public void doStuff() {
        System.out.println("VALUE = " + VALUE.get());
        
        // Creating a scope inside another scope
        Thread.startVirtualThread(() ->
                ScopedValue.runWhere(
                        Task.VALUE, "Inside the Thread", () -> new Task().someOtherTask()
                ));

        someOtherTask(); // current scope

    }

    public void someOtherTask(){
        System.out.println("VALUE = " + VALUE.get());
    }

}

In the previous example the doStuff() runs inside a scope where VALUE is testing. Inside the doStuff() on line 22 a new scope is created that rebinds VALUE to Inside the Thread. Within the nested scope someOtherTask is called and prints VALUE = Inside the Thread When the scope is closed someOtherTask is called again but in the first scope I created. Now it will print VALUE = testing.

Nesting scoped can be a bit confusing, so I hope the following diagram helps you to understand it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

----- Scope where Task.VALUE = "testing"
| new Task().doStuff()
| ... 
| 
| System.out.println("VALUE = " + VALUE.get()); // inside doStuff()
|  ----- Create scope where Task.VALUE = "Inside the Thread"
|  | ... //creating a thread
|  | someOtherTask() // prints VALUE = Inside the Thread
|  -----
|
| someOtherTask(); // prints VALUE = testing
----

Inheriting Scoped Values

Scoped Values are bound to the thread in which they are created. Some use cases require that the data be shared between threads. For example, the data needs to be available to the child threads that are doing work inside the same scope. This is where inheritance comes in.

When you use the StructuredTaskScope scoped values are inherited by the Virtual threads that you from the StructuredTaskScope. In the following example, a scope value is created and inside that scope, new virtual threads are created using the StructuredTaskScope. Each of those three threads will be able to access the scoped value from the parent thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class scopedValuesDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread.startVirtualThread(() ->
                ScopedValue.runWhere(
                        Task.VALUE, "testing", () -> new Task().someOtherTask()
                ));

        Thread.sleep(100);
    }
}

class Task  {

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

    public void someOtherTask(){

        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            scope.fork(() -> new SubTask().runSubtask());
            scope.fork(() -> new SubTask().runSubtask());
            scope.fork(() -> new SubTask().runSubtask());

            scope.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

}

class SubTask{

    String runSubtask() {
        String ScopedValue = Task.VALUE.get();
        System.out.println("Inherited value = " + ScopedValue);
        return ScopedValue;
    }

}

Each of the forked virtual threads prints Inherited value = testing to the console.

Use cases

The following is an example of how you could use Scoped values inside your own project. The scoped value is part of the CurrentUser class and code running inside the scope of the scoped value can access the USERNAME.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class scopedValuesDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread.startVirtualThread(() ->
                ScopedValue.runWhere(
                        CurrentUser.USERNAME, "David", TaskDefinition::runTaskDefinition
                ));

        Thread.sleep(100);
    }
}


class CurrentUser{
    final static ScopedValue<String> USERNAME = ScopedValue.newInstance();
}


class TaskDefinition {

    static void runTaskDefinition(){
        Step step = new Step();
        step.performStep();
    }
    
}

class Step{

    public void performStep() {

        System.out.println("name = " + CurrentUser.USERNAME.get());

    }
}

The great thing about this is that you don't need to pass the username as a parameter throughout the application code but can access it directly where it is needed. This is great for things that run side a scope/ context.

Conclusion

Scoped values are a great addition to the Java language. They help to reduce the memory footprint compared to Thread locals, by making the data immutable. In this tutorial, you learned:

  • What scoped values are
  • How to use them with a single task
  • Use them with virtual threads
  • How the inheritance of scoped values works using the StructuredTaskScope.

With these examples, you should be able to tackle most of the problems out there. If some things are unclear don't hesitate to reach out! I am happy to engage.

Further reading

If you want to know more about Scoped values look at the following Java enhancement proposals (JEPs):