- 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.4 Using Thread Builders to Create Virtual Threads
For greater flexibility in creating threads, the Thread class defines nested interfaces that are builders for creating threads and thread factories (p. 86). In addition, a thread builder provides methods to set thread properties like the thread name, which once set, are valid for all threads and thread factories created with the thread builder. The inheritance hierarchy of these nested interfaces defined in the Thread class is shown in Figure 3.2, where the thread builder subinterfaces Thread.Builder.OfPlatform and Thread.Builder.OfVirtual pertain to platform and virtual threads, respectively.
Figure 3.2 Inheritance Hierarchy of Thread Builders
An implementation of a virtual thread builder (that implements the Thread.Builder.OfVirtual interface) or a platform thread builder (that implements the Thread.Builder.OfPlatform interface) is obtained by calling the static methods ofVirtual() or ofPlatform() of the Thread class, respectively.
Example 3.3 demonstrates using thread builders to create and start threads. The virtual thread builder returned by the Thread.ofVirtual() method has the name property set by the Thread.Builder.OfVirtual.name() method. The threads it creates will have the name "VT_n", where the prefix "VT_" is concatenated with the string representation of n that is the value of the counter that the thread builder employs, starting with the initial value specified together with the prefix in the call to the name() method.
Thread.Builder.OfVirtual vtBuilder = Thread.ofVirtual().name("VT_", 1);
The assignment statement above is equivalent to the following statements:
Thread.Builder.OfVirtual vtBuilder = Thread.ofVirtual();
vtBuilder.name("VT_", 1);// Returned reference to the thread builder is discarded.
The printout shows that the two virtual threads created have the names VT_1 and VT_2 in their default string representation.
Calling the name() method with only the string name will set the same name for all threads created by the thread builder. Note that a virtual thread does not have a name when it is created.
Calling the start() or the unstarted() methods of the thread builder will create a thread when passed a Runnable—that is, the task to execute, but only the thread created by the start() method of the thread builder will be scheduled to begin execution, and the thread created by the unstarted() method of the thread builder must explicitly call its start() method to begin execution. The following code is equivalent to the code at (3):
Thread vt1 = vtBuilder.unstarted(task); Thread vt2 = vtBuilder.unstarted(task); vt1.start(); vt2.start();
Printout from Example 3.3 shows that VT_1 and VT_2 were mounted on carrier threads worker-1 and worker-2 during execution of the task.
When the code at (4) is executed to print the string representation of the virtual threads, we see that thread VT_1 is still in the runnable state but not mounted on any carrier thread. However, thread VT_2 is in the terminated state having completed its execution.
The join() method is necessary to call at (5) to allow the virtual threads to complete their execution.
Creation of platform threads using a platform thread builder is analogous to that for virtual threads, as shown from (6) to (8) in Example 3.3. Waiting to join in the main thread is not necessary for platform threads. The string representation of a platform thread includes the thread ID, the thread name if any, the priority, and the name of the parent thread. The Thread.Builder.ofPlatform interface defines methods to set various properties of a platform thread.
Example 3.3 Using Thread Builders to Create Threads
package vt;
public class ThreadBuilderDemo {
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());
// Obtain a virtual thread builder: (2)
Thread.Builder.OfVirtual vtBuilder = Thread.ofVirtual().name("VT_", 1);
// Creating and starting 2 virtual threads (3)
// using the virtual thread builder:
Thread vt1 = vtBuilder.start(task);
Thread vt2 = vtBuilder.start(task);
// Print virtual thread info: (4)
System.out.println("vt1: " + vt1);
System.out.println("vt2: " + vt2);
// Wait for the virtual threads to join: (5)
vt1.join();
vt2.join();
System.out.println(new StringBuffer().repeat('-', 40));
// Obtain a platform thread builder: (6)
Thread.Builder.OfPlatform ptBuilder = Thread.ofPlatform().name("PT_", 1);
// Creating and starting 2 platform threads (7)
// using the platform thread builder:
Thread pt1 = ptBuilder.start(task);
Thread pt2 = ptBuilder.start(task);
// Print platform thread info: (8)
System.out.println("pt1: " + pt1);
System.out.println("pt2: " + pt2);
}
}
Probable output from the program:
VirtualThread[#22,VT_2]/runnable@ForkJoinPool-1-worker-2: I am on it! VirtualThread[#20,VT_1]/runnable@ForkJoinPool-1-worker-1: I am on it! vt1: VirtualThread[#20,VT_1]/runnable vt2: VirtualThread[#22,VT_2]/terminated ---------------------------------------- Thread[#25,PT_1,5,main]: I am on it! pt1: Thread[#25,PT_1,5,main] Thread[#26,PT_2,5,main]: I am on it! pt2: Thread[#26,PT_2,5,main]
Important Aspects of Virtual Threads
Example 3.4 shows how virtual threads and platform threads are different in various aspects. Two unstarted threads, one virtual thread and one platform thread, are created with names vt and pt using a virtual and a platform thread builder, respectively.
Is a Thread Virtual?
The isVirtual() method of the Thread class determines whether a thread is virtual or not. The output shows that thread vt is virtual but thread pt is not. There is no isPlatform() method in the Thread class.
Virtual Threads are Daemon Threads
The isDaemon() method of the Thread class determines whether a thread is daemon or not. The output shows that thread vt is daemon but thread pt is not. Trying to set a virtual thread as non-daemon with the setDaemon(false) call results in an IllegalArgumentException.
Virtual Threads have Normal Priority
Virtual threads always have ThreadPriority.NORM_PRIORITY (=5) that cannot be changed. Setting a different priority of a virtual thread with the setPriority() method is ignored. The method can readily be used to change the priority of a platform thread.
Virtual Threads belong to VirtualThreads Group
All virtual threads belong to the VirtualThreads group, whereas a platform thread belongs in a specific thread group. A thread group allows its thread to be manipulated collectively rather than individually. The output shows that thread vt belongs to the VirtualThreads group, whereas thread pt belongs to the main thread group.
Example 3.4 Selected Aspects of Threads
package vt;
public class ThreadAspects {
public static void main(String[] args) throws InterruptedException {
// Create task:
Runnable task = () -> System.out.println(Thread.currentThread());
// Create threads:
Thread vt = Thread.ofVirtual().name("vt").unstarted(task);
Thread pt = Thread.ofPlatform().name("pt").unstarted(task);
// Get names:
String vtName = vt.getName();
String ptName = pt.getName();
// Virtual:
System.out.println(vtName + " virtual? " + vt.isVirtual());
System.out.println(ptName + " virtual? " + pt.isVirtual());
// Daemon:
System.out.println(vtName + " daemon? " + vt.isDaemon());
// vt.setDaemon(false); // java.lang.IllegalArgumentException:
// can only be true for virtual threads
System.out.println(ptName + " daemon? " + pt.isDaemon());
// Priority:
System.out.println(vtName + " priority (before change): " +
vt.getPriority()); // NORM_PRIORITY = 5
vt.setPriority(6);
System.out.println(vtName + " priority (after change): " +
vt.getPriority()); // Unchanged: NORM_PRIORITY
System.out.println(ptName + " priority (before change): " +
pt.getPriority()); // NORM_PRIORITY = 5
pt.setPriority(4);
System.out.println(ptName + " priority (after change): " + pt.getPriority());
// ThreadGroup:
System.out.println("Thread group for " + vtName + ": " +
vt.getThreadGroup().getName());
System.out.println("Thread group for " + ptName + ": " +
pt.getThreadGroup().getName());
vt.start();
vt.join();
pt.start();
}
}
Probable output from the program:
vt virtual? true pt virtual? false vt daemon? true pt daemon? false vt priority (before change): 5 vt priority (after change): 5 pt priority (before change): 5 pt priority (after change): 4 Thread group for vt: VirtualThreads Thread group for pt: main VirtualThread[#20,vt]/runnable@ForkJoinPool-1-worker-1 Thread[#21,pt,4,main]
