Modern Asynchronous Technologies (Executor, Virtual Threads)
In Java, methods for providing a more efficient and modern multi-threading environment have evolved far beyond simply instantiating and managing Thread instances directly. The ExecutorService and the Virtual Threads officially introduced in Java 21 are prime examples.
1. Thread Pool built upon the Executor Framework
The process of creating and destroying threads consumes significant system resources. Creating an infinite number of threads for every incoming request can cause an application to crash.
To prevent this, the concept of a Thread Pool was introduced. It pre-creates multiple threads and places them in a pool. When a task arrives, a thread is taken from the pool; once the task is finished, the thread is returned. The ExecutorService in the java.util.concurrent package provides an easy way to implement this.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create a fixed thread pool with a size of 5
ExecutorService executor = Executors.newFixedThreadPool(5);
for(int i = 0; i < 10; i++) {
final int taskNum = i;
executor.execute(() -> {
String threadName = Thread.currentThread().getName();
System.out.println("Processing task " + taskNum + ": " + threadName);
});
}
// Shut down the thread pool when all tasks are processed
executor.shutdown();
}
}
2. CompletableFuture (Java 8)
This feature resolves the drawbacks of callback-style programming, allowing asynchronous tasks to be composed or exceptions handled gracefully through chaining. It operates very similarly to JavaScript Promises.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "Async Task Result";
}).thenApply(result -> {
return result + " -> Processed";
});
System.out.println(future.get()); // Output: Async Task Result -> Processed
}
}
3. Virtual Threads (Java 21+)
Virtual Threads are lightweight threads newly added to the Java platform. Traditional OS-level threads mapped 1:1 with Java threads (Platform Threads) were heavy, making it difficult to create tens of thousands of them. However, Virtual Threads have extremely low memory and context-switching overhead, meaning you can easily create millions concurrently.
How to use Virtual Threads
import java.util.concurrent.Executors;
public class VirtualThreadExample {
public static void main(String[] args) throws Exception {
// Method 1: Creating a Virtual Thread
Thread.ofVirtual().name("my-virtual-thread").start(() -> {
System.out.println("Virtual thread is running");
}).join();
// Method 2: Creating an Executor that uses a new virtual thread for each task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for(int i = 0; i < 100000; i++) { // Creating 100,000 threads with ease
executor.submit(() -> {
try {
Thread.sleep(1000); // Switching occurs when blocked, allowing other threads to run
System.out.println("Task completed: " + Thread.currentThread().getName());
} catch (InterruptedException e) {}
});
}
} // Automatically shuts down at the end of try-with-resources
}
}
Virtual threads bring massive performance gains in modern backend applications, especially for I/O-bound tasks (network communication, DB queries) where threads spend a lot of time in a blocked state.