Contents

Use virtual threads as tasks

Writen by: David Vlijmincx

Introduction

In this article, we dive into implementing virtual threads not as threads or resources but as tasks that have business logic. The idea is from this talk about virtual threads given by Ron Pressler and Alen Bateman. We will look into the benefits of using virtual threads this way and what this would look like in code.

Short introduction of Virtual Threads

Virtual threads are a new kind of thread that is still a preview feature in Java 19 and 20. Virtual threads will not replace the current thread implementation we are already familiar with. Instead, it's an alternative implementation of the thread class that we developers, can use.

The big benefit of virtual threads is that they are very cheap to create because they use very few resources. This is possible because they are just a concept living inside a JVM; the OS knows nothing about virtual threads. Because virtual threads live inside the JVM their memory usage can grow and shrink as needed. The threads we are already familiar with, now called platform threads, are just thin wrappers around OS threads that use much more memory.

The difference in memory usage between virtual threads and platform threads makes it possible to have millions of virtual threads at the same time. If you want to have the same number of platform threads, you need a lot more memory inside your system.

Using virtual threads as tasks

As mentioned in the short introduction, we can have a huge number of virtual threads at the same time. Having a huge number of them is when you will see their biggest benefit over platform threads. If you only convert a small number of platform threads to virtual threads, you will see no performance improvement.

The best way to implement virtual threads is not to replace your current platform threads with virtual threads but to think of virtual threads as a task that can run concurrently. We define a task as a collection of operations that are executed sequentially. The best thing about using virtual threads as a task is that you think of them in terms of resources; virtual threads are really that cheap to create.

There is also no need to share or pool virtual threads because they use so few resources. This makes it possible for tasks and subtasks to create virtual threads when needed. You can still manage these virtual threads using structured concurrency but never pool or share them. Like is said during the talk of Ron Pressler and Alen Bateman, if you find yourself pooling or sharing virtual threads, you should reconsider your design.

What it looks like in code

You have read a lot about how great virtual threads are, especially if you think of them in terms of tasks. So let us see what this looks like in code! Creating virtual threads is luckily very simple with the newVirtualThreadPerTaskExecutor. This ExecutorService creates a new virtual thread for each task you submit.

In the following example, we use this new ExecutorService inside a try-with-resource statement and submit two tasks. Each of these two tasks will run inside a new virtual thread. When these virtual threads are done running, they will be discarded. There is no pooling or sharing of virtual threads when you use this executor.

1
2
3
4
5
6
7
Runnable task1 = () -> System.out.println("task1");
Runnable task2 = () -> System.out.println("task2");

try (ExecutorService e = Executors.newVirtualThreadPerTaskExecutor()) {
    e.submit(task1);
    e.submit(task2);
}

Conclusion

Virtual threads are at their best when you have many of them. This is a different way of thinking than we are used to with platform threads. To make this mind-shift easier, you can also think of virtual threads as tasks and use the newVirtualThreadPerTaskExecutor can also help you move to use threads as tasks. The most important thing to remember is that having many virtual threads is fine, and you never need to share or pool them.

Further reading

More about virtual threads in Java: