JavaJune 28, 20268 min read

Deep Dive into Java Concurrency: Mastering Virtual Threads

Anshu Gupta

Anshu Gupta

AI Product Developer & Software Architect

Deep Dive into Java Concurrency: Mastering Virtual Threads

Introduction

For decades, concurrent JVM development forced a hard compromise: write simple blocking code that wastes system memory on heavy OS threads, or write complex, error-prone reactive code to conserve system resources.

With Java Virtual Threads (introduced as a core feature in JDK 21), that compromise is gone. We can now write clean, blocking code that achieves reactive-level concurrency.

The Problem with Platform Threads

Traditionally, every java.lang.Thread was mapped directly to a native Operating System (OS) thread. These are known as Platform Threads:

  • Size: An OS thread allocates a stack of 1MB by default, meaning 1,000 threads consume 1GB of memory.
  • Context Switches: Switching between platform threads requires costly kernel context switches, which degrade CPU cycle efficiency.
  • Scale: You are typically capped at a few thousand threads per server.

Enter Virtual Threads (Project Loom)

Virtual Threads are lightweight threads managed entirely by the Java Runtime (JVM) instead of the OS kernel.

Instead of allocating a dedicated OS thread, the JVM runs virtual threads on a small pool of platform carrier threads:

  • Memory Efficiency: Virtual thread metadata is stored in the JVM heap, requiring only a few hundred bytes per thread.
  • Mounting and Unmounting: When a virtual thread encounters a blocking operation (like database calls or socket reads), the JVM automatically *unmounts* it from its carrier thread and schedules another task. The carrier thread never blocks!

Writing Thread-per-Request Servers

Spawning a virtual thread is extremely simple. Rather than pooling threads, we simply allocate a new thread per request:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}

In this snippet, we effortlessly spin up 100,000 virtual threads sleeping concurrently. Running this with platform threads would instantly crash the JVM with an OutOfMemoryError.

Best Practices and Pitfalls

While virtual threads are revolutionary, they require a shift in developer patterns:

  • Avoid Thread Pooling: Virtual threads are disposable. Never pool them. Create them on-demand.
  • Avoid ThreadLocal Abuse: Since you can easily run millions of virtual threads, holding large objects in ThreadLocal storage will rapidly consume the JVM heap.
  • Beware of Pinned Threads: A virtual thread can become *pinned* to its carrier thread if it executes inside a synchronized block. To avoid blocking the carrier thread, replace synchronized locks with ReentrantLock.

Conclusion

Java Virtual Threads represent a structural leap in backend concurrency. By eliminating thread lifecycle limitations, JDK 21 allows engineers to build high-throughput microservices using simple, legible synchronous programming styles.

Anshu Gupta
Written By

Anshu Gupta

AI Product Developer & Software Architect

Engineering high-performance software systems where machine learning models and visual interfaces merge. Chair of IEEE Electronics and builder of digital tools.