Backend Development 22 min read

Why Java 19 Virtual Threads Outperform Traditional Threads and Go Goroutines

This article introduces Java 19's virtual threads, compares their performance and scheduling with traditional platform threads and Go goroutines, provides practical code examples, benchmark results, and migration guidelines, and explains the underlying M:N scheduling mechanisms that make virtual threads more efficient for high‑concurrency, I/O‑bound workloads.

macrozheng
macrozheng
macrozheng
Why Java 19 Virtual Threads Outperform Traditional Threads and Go Goroutines

Java Thread Model

Traditional Java threads map one‑to‑one with operating‑system kernel threads, causing significant overhead when many threads are created because each thread consumes system resources and incurs context‑switch costs.

To address this, Java 19 introduces virtual threads (also called lightweight threads). Platform threads remain as before, while a large number of virtual threads are multiplexed onto a smaller pool of platform threads (M:N scheduling), allowing many more concurrent tasks with lower resource consumption.

Creating Java Virtual Threads

New Thread‑related APIs

Thread.ofVirtual()

and

Thread.ofPlatform()

are the new factory methods for creating virtual and platform threads respectively.

<code>// Output thread ID (includes virtual and platform threads) – Thread.getId() is deprecated in JDK 19
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
// Create a virtual thread
Thread vt = Thread.ofVirtual().name("testVT").unstarted(runnable);
vt.start();
// Create a platform thread
Thread pt = Thread.ofPlatform().name("testPT").unstarted(runnable);
pt.start();</code>

For quick creation and start, use

Thread.startVirtualThread(Runnable)

:

<code>Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
Thread vt = Thread.startVirtualThread(runnable);
</code>

Virtual threads can be queried with

Thread.isVirtual()

, and they support the usual

join

and

sleep

operations.

<code>Runnable r = () -> System.out.println(Thread.currentThread().isVirtual());
Thread vt = Thread.startVirtualThread(r);
</code>

An executor that creates a new virtual thread per task is available via

Executors.newVirtualThreadPerTaskExecutor()

:

<code>try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> System.out.println("hello"));
}
</code>

Because virtual threads are a preview feature in JDK 19, they must be compiled and run with the

--enable-preview

flag:

Compile:

javac --release 19 --enable-preview Main.java

Run:

java --enable-preview Main

Performance Comparison: Platform vs. Virtual Threads

A benchmark runs 10,000 one‑second sleep tasks using three approaches: a cached thread pool (unlimited platform threads), a fixed pool of 200 platform threads, and a virtual‑thread executor.

<code>ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
// ... code to print OS thread count ...
</code>

Results:

Cached thread pool: out‑of‑memory after ~3,900 OS threads, execution time > 50 s.

Fixed pool (200 threads): 207 OS threads, execution time ≈ 50 s.

Virtual threads: only 15 OS threads, execution time ≈ 1.6 s.

The data shows virtual threads achieve dramatically higher throughput with far fewer OS threads, especially for I/O‑bound workloads.

Comparison with Go Goroutines

Both Java virtual threads and Go goroutines use M:N scheduling. Example code demonstrates similar logic in both languages, though Go’s

go

keyword is more concise.

<code>// Go example
package main
import ("fmt"; "time")
func say(s string) { for i:=0;i<5;i++ { time.Sleep(100*time.Millisecond); fmt.Println(s) } }
func main() { go say("world"); say("hello") }
</code>
<code>// Java example
public final class VirtualThreads {
    static void say(String s) throws InterruptedException {
        for (int i=0;i<5;i++) { Thread.sleep(Duration.ofMillis(100)); System.out.println(s); }
    }
    public static void main(String[] args) throws InterruptedException {
        var vt = Thread.startVirtualThread(() -> say("world"));
        say("hello");
        vt.join();
    }
}
</code>

Go also provides channels for communication, while Java uses blocking queues (e.g.,

ArrayBlockingQueue

) to achieve similar patterns.

Scheduling Internals

Go’s G‑M‑P model uses a global run queue (GRQ) and per‑processor local queues (LRQ) with work‑stealing. Java’s virtual‑thread scheduler is a FIFO

ForkJoinPool

that maps virtual threads onto platform threads; it also employs work‑stealing to avoid thread starvation, mirroring Go’s approach.

When a virtual thread blocks on I/O or a

BlockingQueue.take()

, it is unmounted from its platform thread and later remounted, allowing the platform thread to execute other work. Certain operations (e.g., inside

synchronized

blocks or native calls) keep the virtual thread fixed to its platform thread, which can reduce throughput.

Best Practices and Migration

Replace existing thread‑pool‑based executors with

Executors.newVirtualThreadPerTaskExecutor()

or use virtual threads directly.

Avoid pooling virtual threads; they are cheap enough to create per task.

Prefer

ReentrantLock

over

synchronized

to prevent virtual threads from being fixed.

Be cautious with

ThreadLocal

in massive virtual‑thread scenarios due to memory overhead.

Conclusion

The article explains Java’s thread model, how virtual threads work, their performance advantages over platform threads, and how they compare to Go’s goroutines. Virtual threads are a preview feature in JDK 19 and may become standard in future releases, offering a powerful tool for high‑concurrency, I/O‑bound applications.

javaConcurrencyGovirtual threadsJDK19
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.