JDK 16 early access has a build available including Project Loom which is all about virtual, light-weight threads (also called Fibers) that can be created in large quantities, without worrying about exhausting system resources.

Project Loom is also the reason why I did not use a reactive framework for JobRunr as it will change the way we will write concurrent programs. Project Loom with it’s Virtual Threads is supposed to be a drop-in replacement for the existing threading framework and I tried it out today using JobRunr. This also means that JobRunr, as of v0.9.16 (to be released soon), will support project Loom out-of-the-box while still also supporting every JVM since Java 8!

Implementing support for Project Loom was easier than I thought using a ServiceLoader. I extracted a simple interface called JobRunrExecutor from the existing ScheduledThreadPool.

public interface JobRunrExecutor extends Executor {

    Integer getPriority();

    void start();

    void stop();

}
The `JobRunrExecutor` interface which is implemented by the existing ScheduledThreadPool

I then created another implementation of the interface using JDK 16 making use of Project Loom which does nothing more than delegating to a Virtual Thread:

public class VirtualThreadJobRunrExecutor implements JobRunrExecutor {

    private static final Logger LOGGER = LoggerFactory.getLogger(VirtualThreadJobRunrExecutor.class);

    @Override
    public Integer getPriority() {
        return 5;
    }

    @Override
    public void start() {
        LOGGER.info("JobRunrExecutor of type 'VirtualThreadJobRunrExecutor' started");
    }

    @Override
    public void stop() {
        // nothing to do
    }

    @Override
    public void execute(Runnable runnable) {
        Thread.startVirtualThread(runnable);
    }
}

Using a standard ServiceLoader I was then able to inject the VirtualThreadJobRunrExecutor thus adding support for Virtual Threads!


private JobRunrExecutor loadJobRunrExecutor() {
    ServiceLoader<JobRunrExecutor> serviceLoader = ServiceLoader.load(JobRunrExecutor.class);
    return stream(spliteratorUnknownSize(serviceLoader.iterator(), Spliterator.ORDERED), false)
            .sorted((a, b) -> b.getPriority().compareTo(a.getPriority()))
            .findFirst()
            .orElse(new ScheduledThreadPoolExecutor(serverStatus.getWorkerPoolSize(), "backgroundjob-worker-pool"));
}

With all this in place, it was time to test and see if performance is better.

Performance showdown: Java 11 vs Java 16 without Project Loom vs Java 16 with Project Loom

As I want to make sure performance is as good as it gets, I have some end-to-end tests which I run regularly, which can be found in the following GitHub repository: https://github.com/jobrunr/example-salary-slip. In that project, paychecks are generated for 2000 employees using a Word template and then transformed to PDF.

To compare Java 11, Java 16 and Java 16 with Project Loom, I ran this project again and hooked up JVisualVM. To give the JVM some time to warm up, I ran each test 3 times.

Comparing performances is not fair as JobRunr only checks for new jobs every 15 seconds and thus comparing these numbers just depends on the fact when I enqueued the jobs. Just to be complete, you can find the numbers below:

Run Java 11 Java 16 Java 16 with Loom
1 140 146 151
2 132 167 139
3 139 167 137
All numbers are in seconds.

What we can compare is the results from JVisualVM. And boy, are these worthwhile!

JDK 11.0.8
JDK Build 16-loom+5-54 without Virtual Threads
JDK Build 16-loom+5-54 with Virtual Threads

The biggest difference is memory usage:

  • JDK 11 occupied a heap of 6.8 GB with a peak use of 4.7 GB
  • JDK 16 without Virtual Threads occupied a heap of 4.6 GB with a peak use of 3.7 GB
  • JDK 16 with Virtual Threads occupied a heap of 2.7 GB with a peak use of 2.37 GB

So, using JDK 16 with these light-weight virtual threads resulted in:

  • only 50% usage of heap memory compared to JDK 11
  • only 64% usage of heap memory compared to JDK 16 without virtual threads

Conclusion

While I initially thought that Project Loom would increase performance a lot, I currently see major improvements in memory usage. I was surprised as how easy it was to support Project Loom thanks to the use of the ServiceLoader.

Do note that JDK 16 is an early-access build and it’s not even sure if Project Loom will be part of JDK 16.