Class Experiment01_Overview

java.lang.Object
net.kolotyluk.loom.Experiment01_Overview

public class Experiment01_Overview extends Object

Project Loom Overview Experiment

This experiment expands on the previous one, producing the same results, but doing so with a lot more code.

While this is still a very simple experiment, we will look at many of the new Project Loom bells and whistles, using some new best new practices, so that we are well grounded for further experiments. Feel free to ignore most of this for now, but remember it's here to get grounded again.

Virtual Threads

In simple terms, Virtual Threads are a new implementation of the classic Java Thread APIs, but critically, a more efficient implementation. Unlike Kotlin Coroutines, there are no language changes to support Project Loom. However, unlike Kotlin Coroutines, Virtual Threads are supported by new functionality in the JVM. Once Virtual Threads become stable and available, we should likely see a refactoring of many concurrency frameworks to use Virtual Threads, such as Kotlin Coroutines, Scala, Akka, Reactor and other Reactive frameworks.

Structured Concurrency

In addition to Virtual Threads, one of the important new features Project-Loom brings to JDK-18 is Structured Concurrency In a nutshell, Structured Concurrency is like eliminating goto in old style programming languages, creating better discipline in how we fork and join, keeping all such concurrency in a hierarchy of parents and children, or sessions and sub-sessions.

When the flow of execution splits into multiple concurrent flows, they rejoin in the same code block.
StructuredExecutorPREVIEW is the heart of Structured Concurrency which is designed to work with try-with-resources blocks, where the ExecutorService resource is closed at the end of the block. StructuredExecutor.open(String)PREVIEW opens a 'Session' which implies a lifecycle and lifetime. The lifecycle is best described as

  1. Get an ExecutorService instance and open a session, which starts the lifecycle of that session.
  2. Define a Completion Handler from one of each of which implements some actions on how Task completion is handled. These are also known as Completion Policies, and there could be other policies in the future. Note, the act of shutting down not only affects all the Tasks subordinate to this session, but also recursively shuts down any child sessions too, the whole family of forked tasks... children, children of children, etc.
  3. Use StructuredExecutor.fork(Callable, BiConsumer)PREVIEW to fork (spawn) tasks according to the ThreadFactory in StructuredExecutor.open(String,ThreadFactory)PREVIEW. If not specified, the default is Virtual Threads. BiConsumer the Completion Handler.
  4. Optionally, call StructuredExecutor.shutdown() to cancel all uncompleted Tasks. When this method returns, the calling join() will return immediately without blocking/waiting. We may also call this after StructuredExecutor.joinUntil(Instant) as in the code below, where our policy is to shutdown after timeout. Note: this is very different than ExecutorService.shutdown() so there is a bit of paradigm shift here.
  5. Always call StructuredExecutor.join() to wait for the lifecycles of all the spawned tasks to complete. Note, if these child Tasks spawn their own Tasks, those lifecycles must also complete first.
  6. Handle Execution Failures, such as with StructuredExecutor.ShutdownOnFailure.throwIfFailed(); Basically, we need to deal with this before the try-with-resources block implicitly calls close() in the finally stage, because try-with-resources is not flexible enough to handle this situation.
  7. Optionally, collect the results of all Tasks. If there are failures of some, but not all Tasks, handling this is also shown below.
  8. Close the StructuredExecutor resource implicitly, finally completing its lifetime.

Conclusions

The StructuredExecutor we create here is a 'child' node of the Thread running, inheriting the ScopeLocalPREVIEW values of the thread, and each child that is forked becomes a child node of the StructuredExecutor, also inheriting the ScopeLocalPREVIEW values. In this way, we can manage all forked threads as a group in a well-disciplined way. In a sense, the StructuredExecutor and the Thread it is opened from, are the parents of the sibling tasks that are spawned. In short, Project Loom is an attempt to make Concurrent Families less dysfunctional... 😉

Streams and Lazy Evaluation

One trap I stumbled into with this experiement, was forgetting to terminate the Stream I was using to spawn the Tasks. My original code looked like


 var completedResults = futureStream.map(Future::resultNow).toList();
     
Which threw an IllegalStateException because the tasks were not spawned until toList() is called. The problem was not obvious to me, and I had to ask help from the Loom Developers to understand it. So, when spawning tasks via a Stream, always remember to terminate the stream before calling join().

See Also:
  • Constructor Details

    • Experiment01_Overview

      public Experiment01_Overview()
  • Method Details

    • main

      public static void main(String[] args)