Tuesday, January 3, 2023

Project Loom in JDK-19: the benefits but with quirks

A few months have passed already since JDK-19 release, which we talked about previously in details. More and more developers are switching to JDK-19, turning their heads towards Project Loom and starting to play with virtual threads and structured concurrency (despite the incubation / preview status of these features). And it certainly makes sense, sooner or later, the JVM and API changes will be finalized, marking the era of the Project Loom production readiness.

In today's post, we are going to cover some not so obvious quirks (by-products of the Project Loom implementation) you should be aware of (or may run into) while switching to JDK-19 in the green-field or, more importantly, brown-field projects. Those may manifest even if you are not planning to use Project Loom just yet.

Let us kick it off with API changes. The code snippet below uses old fashioned java.lang.Thread class to spawn some work aside. The computation uses the instance of the Builder inner class, the implementation of the Builder::build() method is left off since it is not really important.

public class Starter {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            public void run() {
                final Builder builder = new Builder();
                builder.build();
            }
        };
        thread.start();
        thread.join();
    }

    private static class Builder {
        public void build() {
            // implementation details
        }
    }
}

The code compiles and runs just fine on any modern JDK, predating JDK-19. On JDK-19 however, it fails to compile, with somewhat cryptic error.

Unresolved compilation problems: 
	Cannot instantiate the type Thread.Builder
	The method build() is undefined for the type Thread.Builder

The rare example of how existing code may clash with API changes: as part of the Project Loom, the java.lang.Thread got a new public sealed interface Builder, which is being rightly picked by the compiler (instead of our Builder class) inside Thread's the subclass. The fix is easy (but may not look pretty), just use the qualified class name:

public class Starter {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread() {
            public void run() {
                final Starter.Builder builder = new Starter.Builder();
                builder.build();
            }
        };
        thread.start();
        thread.join();
    }

    private static class Builder {
        public void build() {
            // implementation details
        }
    }
}

Please notice that nonetheless JDK's preview features were not enabled, the preview APIs are still visible and taken into the consideration by the compiler. The issue has been reported (JDK-8287968) and the possible incompatibilities have been documented (JDK-8288416).

The next quirk we are going to look at is also related to java.lang.Thread but this time we would be using thread pools (executors) from the standard library. Let us assume we need an executor instance which tracks the moment when the new thread is started. One of the options to accomplish that is to use custom java.util.concurrent.ThreadFactory and override Thread::start() method.

public class Starter {
    public static void main(String[] args) throws Exception {
        final ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r) {
                    @Override
                    public void start() {
                        System.out.println("Thread has started!");
                        super.start();
                    }
                };
            }
        });
        
        executor.submit(() -> {}).get(1, TimeUnit.SECONDS);
        executor.shutdown();
    }
}

On JDKs prior to JDK-19, the expected message will be printed out in the console.

Thread has started!

But not in JDK-19: in scope of the Project Loom implementation, the thread pools and executors (notably ForkJoinPool and ThreadPoolExecutor) do not call Thread::start() method anymore. It does not matter if the preview features are enabled or not, and sadly, there is no workaround to simulate the desired behavior (the alternative Thread::start(ThreadContainer) replacement is not accessible). The issue has been reported and is still open as of today (JDK-8292027).

Great, so far we have seen some quirks caused by Project Loom irrespective of the fact it is used or not. Moving on, let us quickly summarize the constraints you may run into when using Project Loom (by enabling JDKs preview features) and virtual threads.

  • be aware of the limitations using synchronized blocks or methods in scope of virtual threads
  • be aware of the limitations using native methods or foreign functions in scope of virtual threads
  • be aware of the limitations some APIs (like file system) in the JDK have when called in scope to virtual threads

Two JEPs, the JEP-425: Virtual Threads (Preview) and JEP-436: Virtual Threads (Second Preview) offer quite a comprehensive overview with respect to the virtual thread implementation and limitations in JDK-19 and upcoming JDK-20, worth of your time reading them. Another good source I would recommend is Coming to Java 19: Virtual threads and platform threads published by Java Magazine last May.

It is fair to say that Project Loom is evolving really fast, and this is the kind of the feature JVM really screamed for. Yes, it has some limitations now, but there are high chances that in the future most of them will be lifted or worked through.

I πŸ‡ΊπŸ‡¦ stand πŸ‡ΊπŸ‡¦ with πŸ‡ΊπŸ‡¦ Ukraine.