A Practical Guide to Using CompletableFuture with Virtual Threads in Java.

Learner, Love to make things simple, Full Stack Developer, StackOverflower, Passionate about using machine learning, deep learning and AI
Search for a command to run...

Learner, Love to make things simple, Full Stack Developer, StackOverflower, Passionate about using machine learning, deep learning and AI
No comments yet. Be the first to comment.
Move beyond traditional RESTful thinking. Learn how to design APIs specifically for MCP (Model Context Protocol) servers. This guide covers the shift in mindset, a practical OpenAPI 3.1 example, and a Spring Boot implementation to make your services ...

Extending Kestra to Every Corner of Your Data Stack. Introduction: The Power of Plugins Imagine you're a master chef. You don't just have one knife - you have specialized tools for every task: a paring knife for delicate work, a chef's knife for chop...
Mastering Complex Orchestration Scenarios. Introduction: The Orchestrator's Toolkit Imagine you're conducting a symphony. You don't just wave your baton - you cue sections, adjust tempo, handle surprises, and ensure harmony. That's what advanced work...
From Data Extraction to Loading - A Practical Guide Introduction: Why ETL Still Matters in the Modern Data Stack Remember when data engineering was "extract, transform, load"? Some say ETL is dead, replaced by ELT, reverse ETL, and data mesh. But her...
Building Blocks of Declarative Orchestration. Introduction: The Power of Simplicity Imagine trying to build a house without understanding bricks, beams, and blueprints. That's what using an orchestration tool without understanding its core concepts f...
With the introduction of Virtual Threads in Java (via Project Loom), asynchronous programming is now simpler and more efficient than ever. CompletableFuture was already a powerful tool for managing asynchronous tasks, but combining it with virtual threads opens new possibilities for handling high-concurrency workloads without the traditional overhead of platform (OS) threads.
In this guide, we’ll explore how to use CompletableFuture with virtual threads, covering practical examples and explaining how they improve the performance, scalability, and readability of asynchronous code.
CompletableFuture is part of Java’s java.util.concurrent package and provides a way to handle async tasks, making it easier to work with multiple threads.
Virtual Threads are lightweight, memory-efficient threads that are managed by the JVM, allowing applications to spawn millions of threads without consuming excessive resources.
By leveraging both, you can create highly efficient and easy-to-manage async workflows, particularly for tasks involving I/O or network operations.
A CompletableFuture is often created with methods like CompletableFuture.supplyAsync() or CompletableFuture.runAsync(). To make it run on a virtual thread, you can provide an executor backed by virtual threads.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
public class CompletableFutureWithVirtualThreadExample {
public static void main(String[] args) {
// Creating an executor with virtual threads
var executor = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("Task running in virtual thread: " + Thread.currentThread());
try {
Thread.sleep(1000); // Simulating a blocking I/O operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Task completed");
}, executor);
future.join(); // Wait for completion
executor.close();
}
}
Explanation: Here, we create a CompletableFuture that runs a task in a virtual thread executor, performing a simple sleep to simulate I/O. The virtual thread ensures that the program can efficiently manage many such tasks without blocking traditional threads.
One of CompletableFuture’s strengths is its ability to combine multiple async tasks. With virtual threads, managing these tasks becomes even easier and more resource-efficient.
thenApplyAsync and thenCombinepublic class CompletableFutureCombineExample {
public static void main(String[] args) {
var executor = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("Task 1 running on: " + Thread.currentThread());
return 10;
}, executor);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("Task 2 running on: " + Thread.currentThread());
return 20;
}, executor);
CompletableFuture<Integer> combinedFuture = future1.thenCombineAsync(future2, Integer::sum, executor);
combinedFuture.thenAcceptAsync(result -> {
System.out.println("Result of combined tasks: " + result);
}, executor).join();
executor.close();
}
}
Explanation: In this example, two tasks are run asynchronously on virtual threads, then combined using thenCombineAsync(). Using virtual threads minimizes memory usage, making it practical to run large numbers of tasks concurrently without overwhelming system resources.
Handling exceptions in asynchronous tasks is essential. With CompletableFuture, you can manage errors using methods like exceptionally or handle. Virtual threads allow you to handle more tasks without crashing the system, even when some tasks may fail.
exceptionallypublic class CompletableFutureErrorHandlingExample {
public static void main(String[] args) {
var executor = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Executing task on virtual thread: " + Thread.currentThread());
if (true) {
throw new RuntimeException("An error occurred!");
}
return "Completed Task";
}, executor).exceptionally(ex -> {
System.out.println("Handling error: " + ex.getMessage());
return "Fallback Result";
});
System.out.println("Result: " + future.join());
executor.close();
}
}
Explanation: Here, a task is executed on a virtual thread, and if an exception occurs, the exceptionally method catches it and provides a fallback result. Virtual threads allow this code to handle many such tasks concurrently without exhausting system resources.
In real-world applications, you often need to chain multiple async operations. With virtual threads, you can handle these workflows without the performance drawbacks that typically accompany traditional thread-based async operations.
public class CompletableFutureChainingExample {
public static void main(String[] args) {
var executor = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Step 1 on virtual thread: " + Thread.currentThread());
return "Step 1 Result";
}, executor).thenApplyAsync(result -> {
System.out.println("Step 2 on virtual thread: " + Thread.currentThread());
return result + " -> Step 2 Result";
}, executor).thenAcceptAsync(finalResult -> {
System.out.println("Final Result: " + finalResult);
}, executor);
future.join(); // Wait for completion
executor.close();
}
}
Explanation: This example demonstrates a multi-step async workflow using virtual threads. Each step runs on a virtual thread, and the final result is printed at the end. With virtual threads, managing such workflows becomes simpler and more efficient.
Running the same async tasks on platform threads (OS threads) versus virtual threads can show a significant difference in memory usage and responsiveness, especially under high concurrency.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
public class ThreadComparisonBenchmark {
public static void main(String[] args) {
int taskCount = 10_000; // Number of tasks
List<CompletableFuture<Void>> futures = new ArrayList<>();
// Virtual Thread Executor
var executor = Executors.newVirtualThreadPerTaskExecutor();
long start = System.currentTimeMillis();
for (int i = 0; i < taskCount; i++) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Thread.sleep(100); // Simulate I/O task
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, executor);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
long end = System.currentTimeMillis();
System.out.println("Time taken with Virtual Threads: " + (end - start) + " ms");
executor.close();
}
}
Explanation: This benchmark creates 10,000 asynchronous tasks and measures execution time using virtual threads. Virtual threads allow the program to handle this workload more efficiently compared to platform threads, with minimal memory overhead and better scalability.
Using CompletableFuture with virtual threads offers a powerful and efficient way to handle asynchronous tasks in Java, particularly in I/O-heavy and highly concurrent applications. This combination allows Java applications to scale to handle thousands, even millions, of tasks without overwhelming system resources. Key takeaways include:
Scalability: Virtual threads combined with CompletableFuture make it practical to handle massive concurrency.
Error Handling: Easily manage errors in async tasks while maintaining performance.
Improved Code Readability: Virtual threads simplify async programming, making CompletableFuture chains easier to read and maintain.
By using CompletableFuture with virtual threads, Java developers can build high-performance, scalable applications that fully leverage the concurrency capabilities introduced by Project Loom.
More such articles:
https://www.youtube.com/@maheshwarligade