Skip to main content

Java Structured Concurrency

·3 mins

… continued from the previous post.

Structured Concurrency Preview #

I’ve written about structured concurrency, and Java has a preview API1 StructuredTaskScope2:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import com.fillmore_labs.blog.jvt.Slow2;
import java.util.concurrent.StructuredTaskScope;

void main() {
  var deadline = Instant.now().plusMillis(100L);
  try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    for (int i = 0; i < 1_000; i++) {
      // var queryStart = Instant.now();
      scope.fork(
          () -> {
            Slow2.fibonacci(27);
            // var duration = Duration.between(queryStart, Instant.now());
            return null;
          });
    }

      scope.joinUntil(deadline);
  }
}

In Java structured concurrency includes cancelation via thread interruption, aborting the unfinished calculations. We use our old recursive Fibonacci calculation as Slow2, made cancelable with:

    if (Thread.interrupted()) {
      throw new InterruptedException();
    }

When we run this, it exits after around 100 Milliseconds:

> bazel run //:try6
INFO: Running command line: bazel-bin/try6
*** Finished 129 runs (871 canceled) in 113.267ms - avg 50.995ms, stddev 16.769ms

Which shows us that all virtual threads are started, even though we could only finish 129. Extending the deadline to run to completion gives:

*** Finished 1000 runs (0 canceled) in 373.313ms - avg 172.825ms, stddev 92.234ms

flame graph of run 6

So, Thread.interrupted() is not free (it’s the blue areas on top), but performant enough to call it often.

Another Example #

Mirroring our Go experiments we define a task and a function calling it:

 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
41
42
43
44
45
46
47
48
49
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.StructuredTaskScope;

final Duration processingTime = Duration.ofSeconds(1);

void main() throws Exception {
  try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var start = Instant.now();

    scope.fork(
        () -> {
          task("task1", processingTime.dividedBy(3), null);
          return null;
        });

    scope.fork(
        () -> {
          task("task2", processingTime.dividedBy(2), new TestException("task2 failed"));
          return null;
        });

    scope.fork(
        () -> {
          task("task3", processingTime, null);
          return null;
        });

    scope.join();

    var result = scope.exception();
    var duration = Duration.between(start, Instant.now());
    System.out.println(STR."*** Got \"\{result}\" in \{duration}");
  }
}

void task(String name, Duration processingTime, Exception result) throws Exception {
  Thread.sleep(processingTime);

  if (result != null) {
    throw result;
  }
}

static class TestException extends Exception {
  TestException(String message) {
    super(message);
  }
}

Running this, we see similar results as in our previous experiment:

> bazel run //:try7
INFO: Running command line: bazel-bin/try7
*** Got "com.fillmore_labs.blog.jvt.TestException: task2 failed" in 520,398ms

So ShutdownOnFailure closely mimics Go’s errgroup.

Summary #

Java seems to bet on structured concurrency, at least for virtual threads in non-library code. It uses thread interruption as a means of cancelation, which requires having a handle to the running thread. We might eventually see support for context propagation, e.g. from OpenTelemetry, for the new constructs. This is conceptually very different for Go’s context, which is just hierarchically passed down and cancels tasks, including subtasks, regardless of whether the canceler is aware of them.


  1. Ron Pressler, Alan Bateman. 2023. Structured Concurrency (Second Preview). In JDK Enhancement Proposals — September 2023 — JEP 462 — <openjdk.org/jeps/462↩︎

  2. The code is available on GitHub at github.com/fillmore-labs/blog-javavirtualthreads↩︎