Virtual Threads in Java: A Practical Guide.

Virtual Threads in Java: A Practical Guide.

Java's introduction of Virtual Threads in Project Loom is a significant development in concurrent programming. Virtual threads are lightweight, user-mode threads designed to simplify concurrency in Java applications by enabling high levels of scalability with minimal overhead. Unlike traditional (platform) threads, virtual threads are managed by the Java Virtual Machine (JVM) rather than the operating system, allowing for millions of threads to be spawned without the resource-intensive costs of OS threads.

This guide will explore virtual threads practically, covering key concepts, setup, use cases, and practical examples to help you leverage this new concurrency model effectively.


1. What Are Virtual Threads?

Virtual threads are lightweight threads managed by the JVM, enabling high concurrency by decoupling Java threads from OS threads. Unlike traditional threads (known as platform threads), which are limited by system resources, virtual threads allow for massive concurrency as they are managed in user space, enabling millions of concurrent operations without overwhelming the system.

Key Features of Virtual Threads:

  • Minimal overhead: Each virtual thread consumes minimal memory and CPU.

  • Managed by JVM: Virtual threads are scheduled and managed by the JVM, reducing reliance on OS resources.

  • Suitable for IO-bound tasks: They can be suspended without blocking OS threads, making them ideal for applications waiting on network or file I/O.


2. Creating Virtual Threads in Java

Creating virtual threads is straightforward with Java’s new Thread.ofVirtual().start() API.

public class VirtualThreadExample {
    public static void main(String[] args) {
        Thread virtualThread = Thread.ofVirtual().start(() -> {
            System.out.println("Running in a virtual thread: " + Thread.currentThread());
        });

        virtualThread.join(); // Wait for completion
    }
}

Explanation: The Thread.ofVirtual().start() method creates a virtual thread, which then prints a message. The join() method ensures the main thread waits for the virtual thread to complete.


3. Using Virtual Thread Executors

Java also provides a VirtualThreadPerTaskExecutor to manage virtual threads, ideal for cases where a pool of tasks is required.

import java.util.concurrent.Executors;

public class VirtualThreadExecutorExample {
    public static void main(String[] args) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10; i++) {
                int taskId = i;
                executor.submit(() -> {
                    System.out.println("Task " + taskId + " running on " + Thread.currentThread());
                });
            }
        } // Automatically shuts down after use
    }
}

Explanation: Here, we create an executor that spawns a new virtual thread for each task, automatically cleaning up after use. This is beneficial for executing multiple isolated tasks without managing thread creation and lifecycle manually.


4. Comparing Virtual Threads to Platform Threads

Platform threads are limited by system resources and are suitable for CPU-bound tasks, while virtual threads are highly efficient for IO-bound tasks.

FeaturePlatform ThreadVirtual Thread
Creation OverheadHighLow
Context SwitchingManaged by OSManaged by JVM
Best Use CaseCPU-bound tasksIO-bound tasks
Memory ConsumptionHighLow

5. Practical Examples of Virtual Threads

Example 1: Handling High-Concurrency Tasks (e.g., Web Requests)

Virtual threads are perfect for applications that need to handle thousands of concurrent tasks, such as HTTP requests in a server.

import java.util.concurrent.Executors;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

public class HighConcurrencyExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        var executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 0; i < 1000; i++) {
            int requestId = i;
            executor.submit(() -> {
                try {
                    HttpRequest request = HttpRequest.newBuilder(new URI("https://example.com"))
                            .GET().build();
                    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
                    System.out.println("Response for request " + requestId + ": " + response.body());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        executor.close();
    }
}

Explanation: This example sends 1000 HTTP requests concurrently using virtual threads, which is efficient as each request may be suspended during network communication without occupying a platform thread.


Example 2: Parallelizing File I/O

For applications that need to read multiple files simultaneously, virtual threads can offer a performant, non-blocking solution.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.Executors;

public class FileReadExample {
    public static void main(String[] args) throws IOException {
        var executor = Executors.newVirtualThreadPerTaskExecutor();

        String[] fileNames = {"file1.txt", "file2.txt", "file3.txt"};

        for (String fileName : fileNames) {
            executor.submit(() -> {
                try {
                    String content = Files.readString(Paths.get(fileName));
                    System.out.println("Contents of " + fileName + ": " + content);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.close();
    }
}

Explanation: This example reads multiple files concurrently without blocking the main thread. Each file read is performed in a virtual thread, enabling efficient concurrent file processing.


Example 3: Blocking Operations on Virtual Threads

Virtual threads can handle blocking operations without locking the underlying OS thread, making them suitable for high-latency tasks.

import java.util.concurrent.Executors;

public class BlockingOperationExample {
    public static void main(String[] args) {
        var executor = Executors.newVirtualThreadPerTaskExecutor();

        executor.submit(() -> {
            try {
                Thread.sleep(1000); // Blocking operation
                System.out.println("Completed after delay");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        executor.close();
    }
}

Explanation: Here, a virtual thread handles a blocking Thread.sleep() operation without occupying an OS thread. This allows for thousands of such tasks without taxing system resources.


6. Real-Life Use Cases for Virtual Threads

  1. High-Concurrency Web Services: Virtual threads allow handling of thousands of concurrent requests, ideal for scalable REST or WebSocket services.

  2. Event-Driven Microservices: In event-driven architectures, virtual threads can process events concurrently without blocking.

  3. Concurrent Database Queries: Use virtual threads to run multiple database queries in parallel for data aggregation tasks.

  4. Batch Processing Applications: Process large datasets concurrently, such as reading data files or applying transformations to large data streams.


7. Limitations and Best Practices

  • Avoid CPU-Intensive Tasks: Virtual threads excel at IO-bound tasks but may not improve performance for CPU-bound operations.

  • Careful with Legacy Code: Integrating virtual threads into older codebases may require refactoring, especially if the code is platform thread-dependent.

  • Error Handling: Virtual threads handle exceptions in the same way as traditional threads; however, error handling becomes more important when working with high concurrency.


Conclusion

Java’s virtual threads provide a powerful alternative to traditional concurrency models by allowing the creation of high-performance, scalable applications. They’re particularly suited for applications with high levels of concurrency requirements, such as web servers, microservices, and any IO-bound workloads. Through practical examples, we’ve seen how virtual threads simplify concurrency in Java by reducing the resource constraints associated with traditional threads.

More such articles:

medium.com/techwasti

youtube.com/@maheshwarligade

techwasti.com/series/spring-boot-tutorials

techwasti.com/series/go-language

Did you find this article valuable?

Support techwasti by becoming a sponsor. Any amount is appreciated!