Contents

Java virtual threads

Written by: David Vlijmincx

Introduction

In this post, we look at what virtual threads are, what makes them special, and how we can create them.

What are virtual threads

Virtual threads are a new kind of thread added as a preview feature to Java 19. What makes them special is that you can have millions of them simultaneously, whereas the Threads we are used to from previous Java versions take up so much memory that we can only have 30 thousand of them before running out of memory. This all depends on the amount of memory available to the JVM.

Virtual threads take up very little memory because the JVM manages their stack. When we create a virtual thread it needs very little memory because the stack can grow inside the heap as needed.

Virtual threads are called virtual threads because they are not real threads. They are a concept that lives inside the JVM. Just like virtual memory is not real memory, virtual threads are not real threads, but to us, the developer, they act like any other thread.

The threads we are used to working with are now called platform threads. They get this name because they are managed by the platform they run on (the OS). Platform threads take up more memory because the JVM can't grow and shrink the amount memory size of a platform thread. When a platform thread is created, the entire amount of memory that the thread can use is also allocated. Platform threads may end up with more memory than they would ever need. This is a waste of resources.

How to create virtual threads

There are multiple ways to create virtual threads. The easiest way is to use the Thread class. With the introduction of virtual threads, we also get a new factory method to create virtual threads.

The following example shows how we create a platform thread on line 5. On lines 9 and 10, we use the Thread.startVirtualThread() and Thread.ofVirtual().start(task) method to create two virtual threads. In the last example on line 13, we use the factory method to create a virtual thread.

 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();

Creating virtual threads with executor service

Since Java 5, it is encouraged to use the executorService instead of using the Thread class directly. With virtual threads we get a new executorService that creates a new virtual thread for each task you submit.

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

Running millions of virtual threads

As cool as it may sound, you can't run a million threads simultaneously without having a CPU with a core for each thread. Virtual threads run on what is called a carrier thread. A carrier thread is nothing more than a platform thread used to run virtual threads. By default, you will get as many carrier threads as CPU cores. Meaning that if you have an 8-core CPU, you will have 8 carrier threads that can run virtual threads. This means that virtual threads are mapped many-to-many to the number of carrier threads available.

What makes virtual threads so great is that they are good at waiting. When a virtual thread is running on a carrier thread and is being blocked by, for example, a database call. The Virtual thread is unmounted from the carrier thread back into the heap memory. Inside the heap, the virtual thread can wait for the database to return a value while the carrier thread can pick up another virtual thread to run in the meantime.

This makes virtual threads amazing for tasks that use blocking methods. Because waiting on blocked methods is not done on the carrier threads. The carrier thread can keep on running other virtual threads till the virtual thread has a result from the blocking method.

Video

If you want to know more about virtual threads and structured concurrency, you can also watch my talk about the subject on YouTube.

Conclusion

In this article, we looked at virtual threads and what makes them so amazing at what they do. We went in-depth on how virtual threads run inside the JVM and saw examples of how to create virtual threads using the thread class and new executor service.

Further reading

More about virtual threads in Java: