In this post let’s take a closer look at Virtual Threads, one of the most anticipated new Java features of the last couple of years. Virtual Threads are developed by Project Loom, a project team at Oracle that works on new Java features about concurrency and related APIs.

Virtual Threads are not yet a full part of the JVM but are only a preview feature in Java 19. They will most likely be fully released with Java 21, the next scheduled long term support version. If you want to check out any of the preview features, you need to enable previews when you compile and run your code. Check out this post for a guide on how to enable preview features.
Virtual Threads vs Platform Threads
In previous Java versions, the only way to execute code concurrently were so called platform threads which are very closely coupled to the underlying operating system. Platform threads are a great way to distribute calculation load on multi-core processors and are the most common approach in most programming languages to do so.
However, platform threads have some serious drawbacks. They require a relatively large amount of memory and are quite costly to create and destroy. That’s why platform threads are usually grouped in thread pools, from which an application can ask for a idle one and return it after the work is done. With today’s ever growing traffic on web servers, this constraint of a limited thread pool size becomes more and more of a problem, which resulted in the rise of the reactive programming model that tries to mitigate this effect by adding another abstraction layer on top of platform threads that deals with all the cumbersome thread-pool handling operations.
To reduce the need of using platform threads or reactive programming, Oracle came up with the idea of Virtual Threads, which are aimed to eliminate the drawbacks of platform threads, but still give the developer the same imperative programming model that they are used to. If you want to go dive deep into this background, you can find extensive information about on the JEP 425 page.
But enough about the theory for now and let’s look at the code to create and use a virtual thread. Here is a simple example that first starts a virtual and directly after a platform thread:
public static void startThread() throws InterruptedException{
Runnable run = () -> {
System.out.println(Thread.currentThread().toString());
};
Thread virtualThread = Thread.ofVirtual().start(run);
virtualThread.join();
System.out.println("----------------");
Thread platformThread = Thread.ofPlatform().start(run);
platformThread.join();
System.out.println("----------------");
}We first create a Runnable in line three to later to provide some code that will be executed by our two threads. This Runnable simply prints the reference of the current running thread to the console. Next we construct a virtual thread with the static method ofVirtual of the Thread class. Virtual threads are of type VirtualThread, which is a subtype of the Thread class and you can use it like any other Thread by calling its start method. The start method takes a Runnable that will be executed by the thread. Next we call the join method on our virtual thread reference to wait for it to complete before we continue.
In line 9 to 11 we create a platform thread which can be done by the new ofPlatform method of the Thread class. Again we call the start method of the Thread, pass in the previously defined Runnable and call join on the reference to let it complete. As you can see, the handling of virtual threads and platform thread is pretty much the same apart from using a different static factory method of the Thread class. Also all other operations you can use to interact with a thread like sleep, notify and wait work exactly the same way for virtual and for platform threads.
Let’s have a closer look at the output of the above code:
VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
----------------
Thread[#24,Thread-0,5,main]
----------------We can see that the first thread we created is of type VirtualThread, has the id 21 and is in the state runnable. You can see which platform thread is attached to the virtual thread that actually runs it in the background. In this case our virtual thread is executed by a platform thread named „ForkJoinPool-1-worker-1“.
Runtime Performance
Next, we will try to not only start one but a million virtual threads and check out if they are really a lot faster than good old platform threads. If you ever wrote a multithreading application in Java, you will know that even a couple of hundred threads maintained concurrently need way too much memory for smaller machines. Not even the biggest single server that mortals like us can afford can handle a million threads that are actually doing something meaningful. In the following example the threads do very little, so we actually won’t run into any memory problems but will still be able to check if there are any performance benefits.
Here is the code for the 1 million thread test:
public static void startAlotOfThreads() {
Instant begin = Instant.now();
Runnable runner = () -> {
try {
Thread.sleep(10);
System.out.println(".");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
List<Thread> threads = IntStream.range(0, 1_000_000).mapToObj( i -> Thread.ofVirtual().unstarted(runner)).toList();
threads.forEach(thread -> thread.start());
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Instant end = Instant.now();
System.out.println(Duration.between(begin, end).toMillis() + "ms");
}Let’s quickly run through this code again. I first record the current time to later have an idea how long this piece of code needed to finish. Next, I create a Runnable that suspends the thread for 10 milliseconds before printing a single dot into the console. By this we can actually see that stuff is happening while running the code. Suspending it for a little bit is important, because it simulates the blocking of the thread as if it was waiting for a database result set or webservice call to happen. By this, the thread will go back into the thread pool and another idle virtual thread will be picked or resumed.
After this, I create a million unstarted threads into one List object. To only create a virtual thread without starting it immediately use the unstarted method when creating your thread after the ofVirtual method . Next, I start all of them in line 16 and join on all of them in line 18 to 24. This means that line 26 can only be reached when all virtual threads did their job and completed successfully. Finally, I print the time in milliseconds that was required to do all this . So how long did this take on my machine?
.
.
.
2806msAfter printing a million dots into the console it completed in a under three seconds. Not too bad for a million concurrent operations, right?
Let’s try the same code, but this time we will create a million platform threads instead. This means we need to change line 14 to the following code:
List<Thread> threads = IntStream.range(0, 1_000_000)
.mapToObj( i -> Thread.ofPlatform().unstarted(runner)).toList();On my machine, this completes in around 10 seconds. That is a 70% performance increase by simply using virtual threads instead of platform threads and changing a single line of code.
Current Limitations of Virtual Threads
Unfortunately, there are still some limitations on the performance benefits when using virtual threads. Under the hood virtual threads are still run by a platform thread at one point of their lifecycle. The magic happens when a virtual threads blocks because for example you do a lengthy I/O operation are database access. Then the underlying platform thread will switch to another idle virtual thread that is waiting to get a turn to run. By this you need only a small number of costly platform threads to run a large number of virtual threads.
In some circumstances, if the virtual thread calls native code during its task, the platform thread cannot switch to another virtual thread. Oracle is working hard on eliminating most scenarios when problematic native code is called by the JVM and replace it with Java code. One quite common scenario which is still a problem in this preview is, when the virtual thread blocks inside a synchronized block of code. If this is the case, the virtual thread will block like a normal platform threads and you might not see any performance or memory benefits.
That’s it for this first look at virtual threads. Overall this new feature looks very promising and I am very sure it will be quickly adopted by the major Java frameworks and application server vendors once it is released properly.
If you want to check out this example by yourself, you can find the code in this github repo.
Schreibe einen Kommentar