Java - Virtual Threads and Project Loom in Java

Virtual Threads are a modern feature introduced in Java through Project Loom to simplify concurrent programming and improve the scalability of Java applications. Traditional Java threads, also called platform threads, are directly managed by the operating system. Although they are powerful, creating thousands of platform threads can consume a large amount of memory and system resources. Virtual Threads solve this problem by allowing Java applications to create lightweight threads that are managed by the Java Virtual Machine (JVM) instead of the operating system.

Project Loom was introduced to make concurrent programming easier, more efficient, and more scalable. Before virtual threads, developers often used asynchronous programming models such as callbacks, CompletableFuture, or reactive programming frameworks to handle many simultaneous tasks. These approaches improved scalability but often made the code more complex and difficult to maintain. Virtual Threads allow developers to write simple synchronous code while still achieving high scalability.

What Are Virtual Threads

A Virtual Thread is a lightweight thread managed by the JVM. Unlike platform threads, virtual threads are not permanently tied to operating system threads. The JVM schedules many virtual threads on a small number of platform threads internally. This process is known as thread multiplexing.

Virtual Threads are designed to handle tasks that spend time waiting, such as:

  • Database operations

  • File reading and writing

  • Network communication

  • API requests

  • Web server processing

Because they are lightweight, developers can create millions of virtual threads without overwhelming the system.

Traditional Platform Threads

Before understanding Virtual Threads, it is important to understand traditional threads.

Example of creating a platform thread:

public class PlatformThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Running platform thread");
        });

        thread.start();
    }
}

In this model:

  • Each Java thread maps directly to an operating system thread.

  • Creating many threads increases memory usage.

  • Context switching between threads becomes expensive.

  • Large-scale applications may face performance limitations.

Problems with Traditional Threads

Traditional threads work well for small applications but become difficult to manage in highly concurrent systems.

Memory Consumption

Each platform thread requires stack memory, often around 1 MB by default. If an application creates thousands of threads, memory usage increases rapidly.

Context Switching Overhead

The operating system switches CPU execution between threads. Frequent switching causes performance overhead.

Complex Asynchronous Code

To avoid creating many threads, developers use asynchronous programming. However, asynchronous code often becomes difficult to read and debug.

Example using CompletableFuture:

CompletableFuture.supplyAsync(() -> fetchData())
    .thenApply(data -> processData(data))
    .thenAccept(result -> saveResult(result));

Although efficient, this style can become complicated in large applications.

Introduction to Project Loom

Project Loom is an OpenJDK project designed to improve Java concurrency. Its primary goals are:

  • Lightweight concurrency

  • Simpler programming model

  • Better scalability

  • Easier maintenance of concurrent applications

Project Loom introduces:

  • Virtual Threads

  • Structured Concurrency

  • Scoped Values

Among these features, Virtual Threads are the most important and widely used.

Creating Virtual Threads

Java provides simple methods to create virtual threads.

Example 1: Creating a Virtual Thread

public class VirtualThreadExample {
    public static void main(String[] args) throws InterruptedException {

        Thread virtualThread = Thread.startVirtualThread(() -> {
            System.out.println("Running virtual thread");
        });

        virtualThread.join();
    }
}

In this example:

  • startVirtualThread() creates a virtual thread.

  • The JVM manages the thread efficiently.

  • Very little memory is consumed.

Creating Multiple Virtual Threads

One of the biggest advantages is the ability to create large numbers of threads.

public class MultipleVirtualThreads {
    public static void main(String[] args) {

        for (int i = 0; i < 100000; i++) {
            Thread.startVirtualThread(() -> {
                System.out.println("Task executed");
            });
        }
    }
}

Creating 100,000 platform threads would normally cause memory problems, but virtual threads handle this efficiently.

How Virtual Threads Work

Virtual Threads are scheduled by the JVM instead of the operating system.

Carrier Threads

Virtual Threads run on a small pool of platform threads called carrier threads.

The JVM performs the following:

  1. Assigns virtual threads to carrier threads

  2. Suspends virtual threads during blocking operations

  3. Frees carrier threads for other tasks

  4. Resumes virtual threads when needed

This allows efficient resource usage.

Blocking Operations in Virtual Threads

Traditional threads block operating system resources during waiting operations. Virtual Threads behave differently.

Example:

public class BlockingExample {
    public static void main(String[] args) {

        Thread.startVirtualThread(() -> {
            try {
                Thread.sleep(5000);
                System.out.println("Completed");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println("Main thread continues");
    }
}

When the virtual thread sleeps:

  • The JVM suspends the virtual thread

  • The carrier thread becomes free

  • Another virtual thread can use the carrier thread

This improves scalability significantly.

Executor Service with Virtual Threads

Java provides executors specifically for virtual threads.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualExecutorExample {

    public static void main(String[] args) {

        try (ExecutorService executor =
                     Executors.newVirtualThreadPerTaskExecutor()) {

            for (int i = 0; i < 10; i++) {
                executor.submit(() -> {
                    System.out.println(Thread.currentThread());
                });
            }
        }
    }
}

This executor creates a new virtual thread for each task.

Virtual Threads vs Platform Threads

Feature Platform Threads Virtual Threads
Managed By Operating System JVM
Memory Usage High Very Low
Scalability Limited Extremely High
Creation Cost Expensive Cheap
Context Switching OS Level JVM Level
Best For CPU-intensive tasks I/O-intensive tasks

Use Cases of Virtual Threads

Web Servers

Web servers often handle thousands of simultaneous requests. Virtual Threads allow each request to run in its own thread efficiently.

Database Applications

Applications waiting for database responses benefit greatly because waiting does not waste operating system threads.

Microservices

Microservices frequently communicate over networks. Virtual Threads handle network waiting efficiently.

Cloud Applications

Cloud-native applications require scalability. Virtual Threads reduce resource usage and improve throughput.

Performance Benefits

Virtual Threads improve performance in applications with many waiting tasks.

Benefits include:

  • Reduced memory consumption

  • Better scalability

  • Simpler code

  • Easier debugging

  • Improved server throughput

However, Virtual Threads are not always better for CPU-heavy workloads because CPU-intensive tasks still compete for processor time.

Limitations of Virtual Threads

Not Ideal for CPU-Bound Tasks

If tasks heavily use CPU resources, Virtual Threads may not provide major advantages.

Synchronization Issues

Using synchronized blocks incorrectly can pin carrier threads and reduce efficiency.

Example:

synchronized(lock) {
    Thread.sleep(5000);
}

This can block the carrier thread.

Native Calls

Some native operations may block carrier threads.

Best Practices

Use Virtual Threads for I/O Operations

Virtual Threads work best for tasks involving waiting.

Avoid Long Synchronized Blocks

Minimize blocking synchronization.

Prefer Structured Concurrency

Structured concurrency improves thread lifecycle management.

Monitor Application Performance

Even though Virtual Threads are lightweight, performance monitoring is still important.

Structured Concurrency

Project Loom also introduces structured concurrency to simplify thread management.

Example concept:

  • Tasks are grouped together

  • Parent task controls child tasks

  • Easier cancellation and error handling

This improves reliability in concurrent applications.

Real-World Example

Suppose a web server receives 50,000 requests simultaneously.

With platform threads:

  • Huge memory usage

  • Possible thread exhaustion

  • Performance degradation

With Virtual Threads:

  • Lightweight handling

  • Efficient scheduling

  • Better scalability

Each request can be processed in its own virtual thread using straightforward synchronous code.

Virtual Threads in Java Versions

Virtual Threads became officially available in:

  • Preview in Java 19 and Java 20

  • Stable feature in Java 21

Java 21 users can safely use Virtual Threads in production applications.

Conclusion

Virtual Threads and Project Loom represent one of the most significant improvements in Java concurrency. They allow developers to build highly scalable applications using simple thread-based programming models without relying heavily on asynchronous frameworks.

By reducing memory usage and simplifying concurrency, Virtual Threads make Java applications easier to develop, maintain, and scale. They are especially useful in modern server-side systems, cloud applications, APIs, and microservices where handling massive numbers of concurrent requests efficiently is essential.