- 3.1 Motivation for Virtual Threads
- 3.2 Virtual Thread Execution Model
- 3.3 Using Thread Class to Create Virtual Threads
- 3.4 Using Thread Builders to Create Virtual Threads
- 3.5 Using Thread Factory to Create Threads
- 3.6 Using Thread Executor Services
- 3.7 Scalability of Throughput with Virtual Threads
- 3.8 Best Practices for Using Virtual Threads
- Review Questions
3.6 Using Thread Executor Services
An executor service implements the ExecutorService interface that extends the Executor interface. It provides methods that facilitate:
Flexible submitting of tasks to the executor service and handling of results that are returned from executing tasks.
Managing the lifecycle of an executor service: creating, running, shutdown, and termination of the executor service.
The Executors utility class provides two methods newVirtualThreadPerTaskExecutor() and newThreadPerTaskExecutor() to obtain one-thread-per-task executor services. Both the executor services implement the AutoCloseable interface and are thus best deployed in a try-with-resources construct that ensures proper shutdown and termination of the executor service.
Using the Virtual-Thread-Per-Task Executor Service
The method newVirtualThreadPerTaskExecutor() returns an executor service that embodies the one-virtual-thread-per-task model of execution—in other words, it exclusively uses a new virtual thread to execute a task.
In Example 3.6, the code at (2) creates a one-virtual-thread-per-task executor service that will create a virtual thread for each task that is submitted. Note there is no way to set any property of a virtual thread that the executor service creates. For example, we cannot set the name of a virtual thread in this executor service.
Customizing the Thread-Per-Task Executor Service
The method newThreadPerTaskExecutor() returns an executor service that is more customizable by a thread factory for executing one thread per task in general. The kind of threads the executor service will create and deploy depends on the thread factory passed to the method.
In Example 3.6, the code at (3) creates a one-thread-per-task executor service that will create a new virtual thread for each task that is submitted, as it is passed a virtual thread factory that is also customized to use a naming scheme for the virtual threads created. This naming scheme for the virtual threads is reflected in the output.
Note that submitting a task to an executor service using the submit() method is an asynchronous operation—that is, the method returns immediately. The try-with-resources construct used to manage the executor service ensures that there is an orderly shutdown and termination of the executor service when the submitted tasks have completed execution.
The handling of the result returned by a task that is implemented as a Callable<V> object and submitted to an execution service for execution by a virtual thread is no different than if it was by a platform thread, requiring polling of the Future<V> object that receives the result.
Example 3.6 Using One-Thread-Per-Task Executor Service
package vt;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class OneThreadPerTaskExecutorDemo {
public static final int NUM_OF_TASKS = 5;
public static void main(String[] args) throws InterruptedException {
// Create a task: (1)
Runnable task = () -> System.out.printf("%s: I am on it!%n",
Thread.currentThread());
// Using an ExecutorService for running one virtual thread per task: (2)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, NUM_OF_TASKS).forEach(i -> executor.submit(task));
}
System.out.println(new StringBuffer().repeat('-', 69));
// Using a customized virtual thread factory with an ExecutorService (3)
// for running one virtual thread per task.
ThreadFactory vtf = Thread.ofVirtual().name("VT_", 1).factory();
try (ExecutorService executor = Executors.newThreadPerTaskExecutor(vtf)) {
IntStream.range(0, NUM_OF_TASKS).forEach(i -> executor.submit(task));
}
}
}
Probable output from the program:
VirtualThread[#22]/runnable@ForkJoinPool-1-worker-2: I am on it! VirtualThread[#20]/runnable@ForkJoinPool-1-worker-3: I am on it! VirtualThread[#23]/runnable@ForkJoinPool-1-worker-1: I am on it! VirtualThread[#24]/runnable@ForkJoinPool-1-worker-2: I am on it! VirtualThread[#25]/runnable@ForkJoinPool-1-worker-1: I am on it! --------------------------------------------------------------------- VirtualThread[#30,VT_1]/runnable@ForkJoinPool-1-worker-1: I am on it! VirtualThread[#31,VT_2]/runnable@ForkJoinPool-1-worker-5: I am on it! VirtualThread[#32,VT_3]/runnable@ForkJoinPool-1-worker-2: I am on it! VirtualThread[#33,VT_4]/runnable@ForkJoinPool-1-worker-5: I am on it! VirtualThread[#34,VT_5]/runnable@ForkJoinPool-1-worker-2: I am on it!
The Executors Utility Class
The Executors utility class provides the following methods to create executor services that implement the one-thread-per-task model of execution:
