Backend Development 8 min read

Unlock 20× Faster Java Concurrency with JDK21 Virtual Threads

This article explains JDK21's new virtual threads, compares them with traditional thread pools through a 10,000‑thread benchmark, shows over 20‑fold performance gains, and provides practical code examples and configuration tips for Java developers.

macrozheng
macrozheng
macrozheng
Unlock 20× Faster Java Concurrency with JDK21 Virtual Threads

JDK21 was released a month ago, introducing a brand‑new concept called virtual threads in addition to the usual GC updates and enhancements.

What Are Virtual Threads

In the current JDK, every instance of java.lang.Thread is a platform thread . Platform threads run Java code on underlying OS threads and occupy one OS thread for their entire lifetime, limiting their number to the number of OS threads. A virtual thread is also an instance of java.lang.Thread , but it runs Java code on an OS thread without capturing that OS thread for its whole lifetime. This allows many virtual threads to share the same OS thread, achieving effective multiplexing. While platform threads monopolize valuable OS threads, virtual threads do not; their count can far exceed the number of OS threads. Virtual threads are a lightweight implementation provided by the JDK rather than the OS. They are a form of user‑mode thread , similar to Go’s goroutines or Erlang’s processes. Historically, Java’s “green threads” were user‑mode threads that shared an OS thread (M:1 scheduling) but were later superseded by platform threads (1:1 scheduling). Virtual threads use M:N scheduling, mapping many virtual threads onto a few OS threads.

In short, developers can now easily create lightweight virtual threads that simplify programming while fully exploiting hardware performance.

Test

We start with a simple benchmark: 10,000 concurrent tasks each blocking for 1 second using

Thread.sleep(1)

to simulate I/O.

<code>public static void main(String[] args) throws InterruptedException {
    long l = System.currentTimeMillis();
    // normal();
    virtual();
    System.out.println(System.currentTimeMillis() - l);
}
</code>

Running with a conventional fixed‑size thread pool:

<code>public static void normal() throws InterruptedException {
    ExecutorService executor = Executors.newFixedThreadPool(200);
    for (int i = 0; i < 10000; i++) {
        executor.execute(() -> {
            try {
                System.out.println("normal");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
    executor.shutdown();
    executor.awaitTermination(100, java.util.concurrent.TimeUnit.SECONDS);
}
</code>

Result:

Running with JDK21’s virtual‑thread executor:

<code>public static void virtual() throws InterruptedException {
    ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    for (int i = 0; i < 10000; i++) {
        executor.execute(() -> {
            try {
                System.out.println("jdk21");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
    executor.shutdown();
    executor.awaitTermination(100, java.util.concurrent.TimeUnit.SECONDS);
}
</code>

Result:

More than 20× speedup!

Conclusion

The benchmark shows that increasing the size of a traditional fixed‑size thread pool reduces execution time, but after the pool size reaches around 10,000 the benefit plateaus because the workload is I/O‑bound (using

Thread.sleep

) and does not consume CPU.

Traditional thread pools usually set the core pool size to the number of CPU cores (e.g., 4) and the maximum to twice that (e.g., 8). In contrast, a virtual‑thread pool requires no explicit size configuration; the JDK automatically creates an appropriate number of threads to maximize throughput.

Virtual threads are still in preview mode and must be enabled with the JVM argument

--enable-preview

. Future Spring Boot releases may adopt virtual threads as the default for asynchronous processing.

backend developmentVirtual ThreadsJDK21Java ConcurrencyPerformance Test
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.