Contents

Creating Virtual Threads

Written by: David Vlijmincx

Introducing virtual threads

A thread in Java is just a small wrapper around a thread that is managed and scheduled by the OS. Project Loom adds a new type of thread to Java called a virtual thread, and these are managed and scheduled by the JVM.

To create a platform thread (a thread managed by the OS), you need to make a system call, and these are expensive. To create a virtual thread, you don't have to make any system call, making these threads cheap to make when you need them. These virtual threads run on a carrier thread. Behind the scenes, the JVM created a few platform threads for the virtual threads to run on. Since we are free of system calls and context switches, we can run thousands of virtual threads on just a few platform threads.

Creating virtual threads

The easiest way to create a virtual thread is by using the Thread class. With Loom, we get a new builder method and factory method to create virtual threads.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Runnable task = () -> System.out.println("Hello, world");

// Platform thread
(new Thread(task)).start();
Thread platformThread = new Thread(task);
platformThread.start();

// Virtual thread
Thread virtualThread = Thread.startVirtualThread(task);
Thread ofVirtualThread = Thread.ofVirtual().start(task);

// Virtual thread created with a factory
ThreadFactory factory = Thread.ofVirtual().factory();
Thread virtualThreadFromAFactory = factory.newThread(task);
virtualThreadFromAFactory.start();

The example first shows us how to create a platform thread, followed by an example of a virtual thread. Virtual and platform threads both take a Runnable as a parameter and return an instance of a thread. Also, starting a virtual thread is the same as we are used to doing with platform threads by calling the start() method.

Creating virtual threads with the Concurrency API

Loom also added a new executor to the Concurrency API to create new virtual threads. The new VirtualThreadPerTaskExecutor returns an executor that implements the ExecutorService interface just as the other executors do. Let's start with an example of using the Executors.newVirtualThreadPerTaskExecutor() method to obtain an ExecutorService that uses virtual threads.

1
2
3
Runnable task = () -> System.out.println("Hello, world");
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.execute(task);

As you see, it doesn't look different from the existing executors. In this example we use the Executors.newVirtualThreadPerTaskExecutor() to create a executorService. This virtual thread executor executes each task on a new virtual thread. The number of threads created by the VirtualThreadPerTaskExecutor is unbounded.

Can I use existing executors?

The short answer is yes; you can use the existing executors with virtual threads by supplying them with a virtual thread factory. Keep in mind that those executors were created to pool threads because platform threads are expensive to create. Using an executor that pools threads in combination with virtual threads probably works, but it kind of misses the point of virtual threads. You don't have to pool them because they are cheap to create.

1
2
3
4
5
6
7
8
ThreadFactory factory = Thread.ofVirtual().factory();
Executors.newVirtualThreadPerTaskExecutor();
Executors.newThreadPerTaskExecutor(factory); // Same as newVirtualThreadPerTaskExecutor
Executors.newSingleThreadExecutor(factory);
Executors.newCachedThreadPool(factory);
Executors.newFixedThreadPool(1, factory);
Executors.newScheduledThreadPool(1, factory);
Executors.newSingleThreadScheduledExecutor(factory);

On the first line, we create a virtual thread factory that will handle the thread creation for the executor. Next, we call the new method for each executor and supply it the factory that we just created. Notice that calling newThreadPerTaskExecutor with a virtual thread factory is the same as calling newVirtualThreadPerTaskExecutor directly.

Completable future

When we use CompletableFuture we try to chain our actions as much as possible before we call get, because calling it would block the thread. With virtual threads calling get won't block the (OS) thread anymore. Without the penalty for using get you can use it whenever you like and don't have to write asynchronous code. This makes writing and reading Java code a lot easier.

Structured Concurrency

With Threads being cheap to create, project Loom also brings structured concurrency to Java. With structured concurrency, you bind the lifetime of a thread to a code block. Inside your code block, you create the threads you need and leave the block when all the threads are finished or stopped.

1
2
3
4
5
6
System.out.println("---------");
try (ExecutorService e = Executors.newVirtualThreadPerTaskExecutor()) {
    e.submit(() -> System.out.println("1"));
    e.submit(() -> System.out.println("2"));
}
System.out.println("---------");

Try-with-resources statements can use an ExecutorService because project Loom extended Executors with the AutoCloseable interface. Inside the try, we submit all the tasks we need to get done, and we leave the try once the threads finish. The output in the console will look like this:

1
2
3
4
---------
2
1
---------

The second dashed line will never be printed between the numbers because that thread waits for the try-with-resources to finish.

Conclusion

In this post, we looked at what Loom will possibly bring to a future version of Java. The project is still in preview, and the APIs can change before we see it in production. But it's nice to explore the new APIs and see what performance improvements it already gives us.

References and further reading