Project Loom & Virtual Threads in Java

22 March 2021 / 6 min read

Development workspace setup 2021

Disclaimer: This post will mention some terms such as concurrency, threads, multitasking, and others that will be not explained in a detailed way, but will try to add references to other posts or pages about them.

Concurrency in Java is natively managed by using threads (java.lang.Thread) which basically is a wrapper/mapper for a native OS thread. This model fits well in a system that does not need too many threads, however it brings some drawbacks when we want to use them in a large scale, let’s say hundreds or thousands threads.

Why ?

  • Native OS threads must support all programming languages, they are not optimised for a specific one (maybe C 🤔).
  • Expensive context switching.
  • High memory resource usage (stack size).

These items bring issues to escalate java applications using the thread approach to do concurrent jobs using many threads.

E.g. A web server application processing 500 req / sec -> 500 threads -> 500 Mb. Thinking about only threads created for each request, for sure a web server use other threads and memory.

Here is where Project Loom comes as a solution, so first of all let’s define what Project Loom is and what it brings to the Java world.

Note Project Loom is still an ongoing project and all the information, names and definitions about it could change and there is not any official JDK released to work with.

Project Loom is to intended to explore, incubate and deliver Java VM features and APIs built on top of them for the purpose of supporting easy-to-use, high-throughput lightweight concurrency and new programming models on the Java platform - Project Loom Wiki

Project Loom is a platform based solution introducing the concept of Virtual Thread (a.k.a Fiber) that is a lightweight thread managed by the Java Virtual Machine rather than the operating system and it fits with the existing Java APIs allowing synchronous (blocking) code.

Using Project Loom in a Java application

Note This post wants to show a general overview and some notions about how project loom and virtual threads perform rather than showing any kind of micro benchmark or something deeper.

We will use jconsole to monitor the performance and then check how many threads are created, how much memory is used and how the CPU is behaving.

For these tests a MacBook Pro 15” 2018 will be used:

  • 16Gb
  • 2,2 GHz 6-Core Intel Core i7
  • Mac OS Catalina

Since there is not any official release of the JDK including Project Loom, we must use the early-access binaries provided by the project.

We can download them from the official page of the project, these binaries are based on JDK 17 and there are options for Linux, MacOS and Windows binaries. Project Loom Early-Access Builds

Once the binaries are downloaded and set in the PATH we can use the JDK Project Loom:

❯ java --version
openjdk 17-loom 2021-09-14
OpenJDK Runtime Environment (build 17-loom+2-42)
OpenJDK 64-Bit Server VM (build 17-loom+2-42, mixed mode, sharing)

Threads

Let’s do a naive test creating an app that creates 1000 threads and each thread run some random math operations between two numbers during a couple of minutes, and then compare the performance using the usual native threads vs virtual threads.

static class RandomNumbers implements Runnable {

    Random random = new Random();

    @Override
    public void run() {
        for (int i = 0; i < 120; i++) { // during 120 seconds aprox
            try {
                int a = random.nextInt();
                int b = random.nextInt();
                int c = a * b;
                System.out.println("c = " + c); // print to avoid compiler remove the operation
                Thread.sleep(1000); // each operation every second
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Then let’s create the threads.

public static void main(String[] args) throws InterruptedException {

      // Create 1000 native threads 
    for (int i = 0; i < 1000; i++) {
        Thread t = new Thread(new RandomNumbers());
        t.start();
    }

    Thread.sleep(120000);
}

Native threads results

  • Around 1000 threads.
  • CPU usage between 2 and 3 %.
  • Constant memory usage around 150 Mb.

Now let’s do the same operation using virtual threads

public static void main(String[] args) throws InterruptedException {

      // Create 1000 virtual threads 
    for (int i = 0; i < 1000; i++) {
        Thread.startVirtualThread(new RandomNumbers());
    }
 
    Thread.sleep(120000);
}

Virtual threads results

  • Around 30 threads
  • CPU usage under 2%
  • Incremental memory usage from 20Mb to 60 Mb.

Executor services

Now let’s do a test using something more elaborate using Executor service to schedule the threads .

CachedThreadPool Executor service

public static void main(String[] args) throws InterruptedException {

    ExecutorService executor = Executors.newCachedThreadPool();

    for (int i = 0; i < 1000; i++) {
        executor.submit(new RandomNumbers());
    }

    executor.shutdown();

    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
}

This implementation of executor service uses a new thread (native) per task scheduled unless there is a free thread to take it.

These results look pretty similar to the threads approach

  • Around 1000 threads.
  • CPU usage around 2 %.
  • Constant memory usage around 130 Mb.

VirtualThreadExecutor Executor service

public static void main(String[] args) throws InterruptedException {

    ExecutorService executor = Executors.newVirtualThreadExecutor();

    for (int i = 0; i < 1000; i++) {
        executor.submit(new RandomNumbers());
    }

    executor.shutdown();

    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
}

These results show a light decrease of memory and CPU usage.

  • Around 30 threads.
  • CPU usage between 1 and 2 %.
  • Incremental memory usage from 30Mb up to 40 Mb.

Conclusion

We can see how the use of virtual threads could help us when we need to use concurrency using thousands of threads without compromising performance and making optimal use of resources.

Project Loom is promising a good future to Java and its concurrency API to be at the level of other languages that already have lightweight concurrency models.

OpenJDK: Loom