| | |

Article 2: Java Virtual Threads — Building Scalable Enterprise Applications with Modern Concurrency

Reading Time: 2 minutes

Introduction

Traditional Java concurrency relies heavily on platform threads. Each platform thread is backed by an operating-system thread, making large thread counts relatively expensive.

This becomes a scalability challenge when applications spend significant time waiting for database queries, network responses, or external service calls.

Java 21 introduced virtual threads as a finalized feature through JEP 444. Virtual threads make it possible to express high-concurrency workloads using straightforward, blocking Java code without requiring an equally large number of operating-system threads.

Virtual threads are particularly useful for I/O-bound workloads. They do not make CPU-intensive calculations execute faster.

Practical example: Concurrent service calls

Imagine an enterprise application retrieving data from multiple downstream systems.


import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class VirtualThreadDemo {

    static String fetchService(String service)
            throws InterruptedException {

        Thread.sleep(500); // Simulates blocking I/O
        return "Response from " + service;
    }

    public static void main(String[] args)
            throws Exception {

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

            Future<String> customers =
                executor.submit(() -> fetchService("CRM"));

            Future<String> orders =
                executor.submit(() -> fetchService("Orders"));

            Future<String> payments =
                executor.submit(() -> fetchService("Payments"));

            System.out.println(customers.get());
            System.out.println(orders.get());
            System.out.println(payments.get());
        }
    }
}

Here, three independent simulated I/O operations execute concurrently. Instead of waiting approximately 500 milliseconds for each task sequentially, the overall simulated waiting period can overlap.

The total execution time also includes scheduling and other overhead, so timing is not guaranteed.

Scaling to thousands of tasks


import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class VirtualThreadScale {

    public static void main(String[] args)
            throws Exception {

        int tasks = 10_000;

        long start = System.nanoTime();

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

            for (int i = 0; i < tasks; i++) {
                executor.submit(() -> {
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }

        long elapsedMs =
            TimeUnit.NANOSECONDS.toMillis(
                System.nanoTime() - start);

        System.out.println(
            "Completed in " + elapsedMs + " ms");
    }
}

This code demonstrates concurrent scheduling; Thread.sleep() simulates waiting rather than performing real database or HTTP operations.

Virtual threads in Spring Boot

For a compatible application running Java 21 or later, enable:


spring.threads.virtual.enabled=true

Spring Boot supports this property and recommends Java 24 or later for improved virtual-thread behavior.

Home

One important production consideration is that virtual threads do not eliminate downstream capacity limits. A database with a 20-connection pool will still have a limited number of simultaneous database connections, even if the application can handle thousands of virtual threads.

Use connection-pool limits, rate limiting, timeouts, and bounded application-level concurrency where appropriate.

When should virtual threads be used?

Virtual threads are effective for synchronous applications with significant blocking I/O, such as REST integrations, JDBC queries, and request orchestration.

They are not generally a replacement for CPU-bound parallel computation, and they should not be pooled in the same way as platform threads.

Architectural takeaway: Virtual threads can increase concurrency while retaining a readable synchronous programming model, but service capacity must still be engineered end to end.

References: OpenJDK JEP 444: Virtual Threads and Spring Boot Virtual Thread Support.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *