Using Executors.newVirtualThreadPerTaskExecutor() in Java
Java introduced virtual threads with Project Loom to simplify concurrency and improve the scalability of applications. Virtual threads are lightweight and have minimal overhead compared to traditional platform threads. One of the convenient ways to work with virtual threads is through the Executors.newVirtualThreadPerTaskExecutor()
method. This method returns an ExecutorService
that creates a new virtual thread for each submitted task, allowing for easy and efficient concurrent task execution.
In this article, we will learn about Executors.newVirtualThreadPerTaskExecutor()
and how to use it to handle concurrent tasks using virtual threads.
1. Understanding Virtual Threads
Virtual threads in Java are a lightweight type of threading that lets us run many tasks at once without using a lot of system resources. Managed by the JVM, they can be created in large quantities without slowing down our program.
1.1 Overview of Executors.newVirtualThreadPerTaskExecutor()
The Executors.newVirtualThreadPerTaskExecutor()
method is a factory method that provides an ExecutorService
capable of executing each task in a new virtual thread. This approach is useful for applications that need to handle a large number of concurrent tasks, such as web servers, batch processors, or real-time data processing systems.
2. Example Usage
Below is an example demonstrating how to use Executors.newVirtualThreadPerTaskExecutor()
to execute tasks concurrently. This example creates an ExecutorService
, submits several tasks, and then shuts down the executor.
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class VirtualThreadExecutorExample { public static void main(String[] args) { // Create an ExecutorService with virtual threads ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); // Submit tasks to the executor for (int i = 0; i < 10; i++) { int taskNumber = i; executorService.execute(() -> { Thread.Builder builder = Thread.ofVirtual().name("virtual-thread-" + taskNumber); Runnable task = () -> { }; Thread t = builder.start(task); System.out.println("Executing task " + taskNumber + " on " + t.getName()); try { // Simulate task processing with sleep TimeUnit.SECONDS.sleep(1); t.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("Task " + taskNumber + " was interrupted."); } System.out.println("Task " + taskNumber + " completed on " + t.getName()); }); } // Shutdown the executor executorService.shutdown(); try { if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("Executor did not terminate in the specified time."); executorService.shutdownNow(); } } catch (InterruptedException e) { System.err.println("Executor termination interrupted."); executorService.shutdownNow(); } System.out.println("All tasks completed."); } }
In this example, we start by creating an ExecutorService
using Executors.newVirtualThreadPerTaskExecutor()
. This executor service will use virtual threads to handle task execution. We then execute ten tasks with the executor, each of which prints a message, simulates work by sleeping for one second, and prints a completion message.
Each task runs in a separate virtual thread. The use of TimeUnit.SECONDS.sleep(1)
simulates a task that takes some time to complete, allowing us to see the concurrent execution of tasks.
To name the threads, we use the Thread.Builder
interface, specifically Thread.ofVirtual().name("virtual-thread-" + taskNumber)
, to create virtual threads with custom names.
After submitting the tasks, we shut down the executor service using executorService.shutdown()
. We then wait for the termination of all tasks with executorService.awaitTermination(5, TimeUnit.SECONDS)
. If the executor does not terminate within the specified time, we forcefully shut it down using executorService.shutdownNow()
.
When you run the above example, you will see the following output indicating the execution and completion of tasks.
3. Using Virtual Threads to Execute Asynchronous Tasks
Virtual threads can also be used to execute asynchronous tasks efficiently. Below is an example demonstrating how to use an ExecutorService to perform three asynchronous tasks and aggregate their results.
public class AsyncVirtualThreadExample { public static void main(String[] args) throws InterruptedException, ExecutionException { // Create a new virtual thread per task executor ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); try { // Submit three asynchronous tasks Future<String> future1 = executor.submit(() -> performTask("Task 1", 2)); Future<String> future2 = executor.submit(() -> performTask("Task 2", 3)); Future<String> future3 = executor.submit(() -> performTask("Task 3", 1)); // Aggregate results String result1 = future1.get(); String result2 = future2.get(); String result3 = future3.get(); // Print results System.out.println("Results:"); System.out.println(result1); System.out.println(result2); System.out.println(result3); } finally { // Shutdown the executor executor.shutdown(); } } private static String performTask(String taskName, int durationInSeconds) { try { // Simulate a blocking task Thread.sleep(durationInSeconds * 1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return taskName + " was interrupted"; } return taskName + " completed after " + durationInSeconds + " seconds"; } }
In this example, three tasks are submitted to the executor using Future<String> future1 = executor.submit(() -> performTask("Task 1", 2));
. Each task simulates a blocking operation by sleeping for a specified number of seconds. The get()
method is called on each Future
to block and wait for the task’s completion. The results are then aggregated.
When you run the above application, it performs three asynchronous tasks using virtual threads, simulating blocking operations with Thread.sleep()
. Each task is submitted to the executor and executed independently. Here is the expected output:
Results: Task 1 completed after 2 seconds Task 2 completed after 3 seconds Task 3 completed after 1 seconds
4. Conclusion
In this article, we explored using Executors.newVirtualThreadPerTaskExecutor() for concurrent task execution with virtual threads, featuring an example of basic task execution and another example of handling asynchronous tasks with Future
. In conclusion, virtual threads provide a scalable and efficient solution for managing concurrent tasks, making them a valuable feature in Java applications.
5. Download the Source Code
This article provided an example of using newVirtualThreadPerTaskExecutor().
You can download the full source code of this example here: newvirtualthreadpertaskexecutor example