How I Spun Up 5 Million Virtual Threads Without Stalling The JVM.

Aseem Savio
Javarevisited
Published in
5 min readJul 19, 2022

Find an updated version of this post on my personal blog blog.aseemsavio.com 🍻

You’ve seen it right! This article exemplifies how I spun up 5 million Java Virtual threads that each sleep for 100 milliseconds without stalling the JVM on my M1 Macbook Pro. Project Loom makes this possible.

I started by downloading the early access build of JDK 19 from here (as the said version is not released yet as of 17th July 2022).

I prefer using Intellij Idea. I made sure I was running the latest version of Idea. I created a simple Java project with the Project SDK set to JDK 19. Since this was my first time using JDK 19, I had to add the newly downloaded and extracted JDK (that was added to the /Library/Java/JavaVirtualMachines directory) by pressing the “Add JDK” button and then the “Next” button.

I gave the project a suitable name and hit the “Finish” button.

I created a project structure like the following.

Inside App.java, I spun up 1 million Virtual Threads (will upgrade to 5 million later) with the help of the new Thread.startVirtualThread() function.

package com.asavio.loom;

import static java.lang.Thread.startVirtualThread;

public class App {

public static void main(String[] args) {
startVirtualThreads();
}

private static void startVirtualThreads() {
for (int i = 0; i < 1000000; i++) {
startVirtualThread(() -> System.out.println(Thread.currentThread()));
}
}
}

When I ran it, I was greeted with the following error.

java: startVirtualThread(java.lang.Runnable) is a preview API and is disabled by default.(use --enable-preview to enable preview APIs)

To fix this, under Preferences -> Build, Execution, Deployment -> Compiler -> Java Compiler, I unchecked the Use '--release’ option and added the following in the “Additional Command Line parameters” text box, and hit “Apply” and then “OK.”

--enable-preview --source 19

I entered the “Run/Debug Configurations” menu and clicked on “Modify options.” It reveals a bunch of options. I checked the “Add VM Options” under “Java.”

It adds a new field called “VM Options,” to which I added the following, clicked “Apply” and “OK.”

--enable-preview

When I reran it, hurray, it spun up a million virtual threads!

To spice things up, I made each virtual thread sleep for 100 ms.

package com.asavio.loom;

import static java.lang.Thread.startVirtualThread;

public class App {

public static void main(String[] args) {
startVirtualThreads();
}

private static void startVirtualThreads() {
for (int i = 0; i < 1000000; i++) {
startVirtualThread(() -> {
System.out.println(Thread.currentThread());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}

Here’s what I witnessed when I ran it.

Virtual Threads don’t block when sleep() or most other blocking operations are invoked (for example, Future.get()). Instead, they unmount themselves from the platform thread they’re running on when they’re about to be blocked, allowing other virtual threads to mount themselves onto the platform thread and run. The Virtual threads return to the JVM’s scheduler when they’re ready to resume execution (in this case, when the sleep time is exhausted). You can read more about Virtual threads here.

Virtual Threads, by default, are daemon threads, meaning the JVM would NOT wait for these threads to complete. Hence, I collected the threads and joined them to make the JVM wait for the completion of those million threads. Find the updated startVirtualThreads() function below.

package com.asavio.loom;

import java.util.ArrayList;

import static java.lang.Thread.sleep;
import static java.lang.Thread.startVirtualThread;

public class App {

public static void main(String[] args) throws InterruptedException {
System.out.println("Main Started");
startVirtualThreads();
System.out.println("Main Ended");
}

private static void startVirtualThreads() throws InterruptedException {

var threads = new ArrayList<Thread>(1000000);

for (int i = 0; i < 1000000; i++) {
var thread = startVirtualThread(() -> {
System.out.println(Thread.currentThread());
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.join();
}
}

}

This starts and waits for all the 1 Million virtual threads created to complete. All the Virtual Threads would have completed execution before “Main Ended” is printed on the screen.

I removed the rest of the print statements as they were distracting. I also up’ed the number of threads to 5 Million.

private static void startVirtualThreads() throws InterruptedException {

var threads = new ArrayList<Thread>(5000000);

for (int i = 0; i < 5000000; i++) {
var thread = startVirtualThread(() -> {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.join();
}
}

The response was:

Main Started
Main Ended
Process finished with exit code 0

Thus, I was able to spin up 5 Million Virtual Threads on my M1 machine.

--

--

Aseem Savio
Javarevisited

Engineer & Designer of highly concurrent, low-latency, distributed systems 🚀| Builder of fully mature REST APIs and SDKs | Maker & Lover of Pizza 🍕