Contents

How to create your own custom StructuredTaskScope

Written by: David Vlijmincx

Introduction

There are use cases where more than the two built-in shutdown policies is needed. For example, when an x number of threads have finished or a virtual thread has returned a specific result. This article will create a StructuredTaskScope that shuts down when a value that meets specific criteria is returned.

What is a StructuredTaskScope

A StructuredTaskScope manages the lifetime of the threads that are forked from the scope. To create our StructuredTaskScope we extend the StructuredTaskScope class and override the handleComplete(Future<T> future) method. Every finished thread will call this method, so it's a great place to call the shutdown() method when certain criteria are fulfilled. The Shutdown() method will cancel all the remaining running threads.

How to create your own StructuredTaskScope

In the following example, we create a CriteriaScope that extends the StructuredTaskScope<Product>. For example, we pass the Product class as a generic to the StructuredTaskScope. Next, we need to override the handleComplete method, and this Method will take a Future as a parameter of the same type as the generic. In this case, that would be Product.

In the handleComplete method, there is an if that first checks if the virtual thread is done. If so, it will check if the resulting product has a price of less than 100. When the criteria of the if-statement are met, we store the result inside a volatile variable. So we can retrieve it at a later point. The last thing we want to do is to shut down the remaining threads by calling shutdown().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 class CriteriaScope extends StructuredTaskScope<Product> {

    private volatile Product product;

    @Override
    protected void handleComplete(Future<Product> future) {
        if (future.state() == Future.State.SUCCESS && future.resultNow().getPrice() < 100) {
            this.product = future.resultNow();
            shutdown();
        }
    }

    public Product getResult(){return product;}
}

Using your structured scope

Using your StructuredTaskScope scope works the same as any other built-in StructuredTaskScope. In the example below we use the CriteriaScope that we created earlier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void start() throws InterruptedException {
    try (var scope = new CriteriaScope()) {

        Future<Product> fork = scope.fork(OverrideScope::getProduct50);
        Future<Product> fork1 = scope.fork(OverrideScope::getProduct300);
        Future<Product> fork2 = scope.fork(OverrideScope::getProduct200);

        scope.join();

        System.out.println("fork.state() = " + fork.state());
        System.out.println("fork.state() = " + fork1.state());
        System.out.println("fork.state() = " + fork2.state());

        System.out.println("result: " + scope.getResult().getPrice());
    }
}

Conclusion

In this article, we saw how to extend the StructuredTaskScope to create your scope. We overrode the handleComplete method to shut down the scope when a certain result is returned. The last example showed how we could use the scope that we created in our code.

Further reading

More about virtual threads in Java: