OCP Java SE 21 Developer
OCP Java SE 21 Developer
Khalid A. Mughal
Vasily A. Strelnikov
—K.A.M.
This page intentionally left blank
Part I: Contents
Part I: Contents v
Part I: Figures ix
Part I: Tables xi
v
vi PART I: CONTENTS
3 Virtual Threads 73
3.1 Motivation for Virtual Threads 74
3.2 Virtual Thread Execution Model 74
3.3 Using Thread Class to Create Virtual Threads 76
Logging Information during Program Execution 77
Creating and Starting a Virtual Thread 77
3.4 Using Thread Builders to Create Virtual Threads 80
Important Aspects of Virtual Threads 84
Is a Thread Virtual? 85
Virtual Threads are Daemon Threads 85
Virtual Threads have Normal Priority 85
Virtual Threads belong to VirtualThreads Group 85
3.5 Using Thread Factory to Create Threads 86
3.6 Using Thread Executor Services 88
Using the Virtual-Thread-Per-Task Executor Service 88
Customizing the Thread-Per-Task Executor Service 88
The Executors Utility Class 90
3.7 Scalability of Throughput with Virtual Threads 90
3.8 Best Practices for Using Virtual Threads 94
Avoid Pinning of Virtual Threads 94
Pinning in Synchronized Block 95
Avoiding Pinning with a Reentrant Lock 98
Avoid Using Virtual Threads for CPU-Bound Tasks 99
Avoid Pooling of Virtual Threads 100
Minimize Using Thread-Local Variables with Virtual Threads 100
Avoid Substituting Virtual Threads for Platform Threads 100
Review Questions 100
1.1 Inheritance
Chapter 1 1 Hierarchy 3
1.2 The instanceof Pattern Match Operator 4
1.3 Record Patterns 22
1.4 Summary of Patterns 34
2.1 Core Collections
Chapter 2 45 Inheritance Hierarchy 47
2.2 Core Maps Inheritance Hierarchy 57
2.3 Core Concurrent Collections Inheritance Hierarchy 65
2.4 Core Concurrent Maps Inheritance Hierarchy 68
3.1 Virtual3 Thread
Chapter 73 Execution Model 75
3.2 Inheritance Hierarchy of Thread Builders 81
ix
This page intentionally left blank
Part I: Tables
xi
This page intentionally left blank
Part I: Examples
xiii
This page intentionally left blank
Preface to OCP Java SE 21
Developer (Exam 1Z0-830)
xv
xvi PART I: PREFACE
Chapter Topics
Each chapter starts with a short summary of the topics covered in the chapter,
pointing out the major concepts that are introduced.
PART I: PREFACE xvii
Prerequisites
Each chapter starts with a short summary of topics that are prerequisites for the
topics covered in the chapter. Part II: OCP Java SE 17 Developer readily provides
coverage for these prerequisites.
Exam Objectives
Developer Exam Objectives
[0.1] Exam objectives that are covered in each chapter are stated clearly at the beginning
of every chapter.
[0.2] The number in front of the objective identifies the exam objective, as defined by
Oracle. The objectives are organized into major sections, detailing the curriculum
for the exam.
[0.3] The objectives for the Java SE 21 Developer Professional Exam are reproduced
verbatim in Appendix A. This appendix also maps each exam objective to
relevant chapters and sections in Part I and in Part II: OCP Java SE 17 Developer.
Supplementary Topics
• Supplementary topics are Java topics that are not on the exam per se, but which
the candidate is expected to know.
• Any supplementary topic is listed as a bullet at the beginning of the chapter.
Review Questions
Review questions are provided after every major topic to test and reinforce the
material. The review questions predominantly reflect the kind of multiple-choice
questions that can be asked on the actual exam. On the exam, the exact number of
answers to choose for each question is explicitly stated. The review questions in
Part I follow that practice.
Many questions on the actual exam contain code snippets with line numbers to
indicate that complete implementation is not provided, and that the necessary
missing code to compile and run the code snippets can be assumed. The review
questions in Part I provide complete code implementations where possible, so that
the code can be readily compiled and run.
Annotated answers to the review questions are provided in Appendix B in Part I.
Java code in the book is presented in a monospaced font. Lines of code in the exam-
ples or in code snippets are referenced in the text by a number, which is specified
by using a single-line comment in the code. For example, in the following code
snippet, the call to the method doSomethingInteresting() at (1) does something
interesting:
// ...
doSomethingInteresting(); // (1)
// ...
Names of classes and interfaces start with an uppercase letter. Names of packages,
variables, and methods start with a lowercase letter. Constants are in all uppercase
letters. Interface names begin with the prefix I, when it makes sense to distinguish
them from class names. Coding conventions are followed, except when we have
had to deviate from these conventions in the interest of space or clarity.
fully.qualified.Name
A vertical gray bar is used to highlight methods and fields found in the classes
of the Java SE Platform API.
Any explanation following the API information is also similarly highlighted.
To get the maximum benefit from using Part I in preparing for the Java SE 21 Devel-
oper Professional Exam, we strongly recommend installing the latest version (Release
21 or newer) of the JDK and its accompanying API documentation. Part I focuses
solely on the new topics that were finalized in Java SE 21 since the release of Java SE 17.
www.oracle.com/technetwork/java/javase/overview/index.html
The current authoritative technical reference for the Java programming language,
The Java® Language Specification: Java SE 21 Edition, can be found at this website:
http://docs.oracle.com/javase/specs/index.html
Khalid A. Mughal
Khalid A. Mughal is the principal author of this book, primarily responsible for
writing the material covering the Java topics. He is also the principal author of sev-
eral other books on previous versions of the Java certification exam: Java SE 17
OCP (1Z0-829), Java SE 11 OCP (1Z0-819), Java SE 8 OCA (1Z0-808), Java SE 6 (1Z0-
851), and SCPJ2 1.4 (CX-310-035).
Khalid is an associate professor emeritus at the Department of Informatics at the
University of Bergen, Norway, where he was responsible for designing and imple-
menting various courses in informatics. Over the years, he has taught programming
(primarily Java), software engineering (object-oriented system development), data-
bases (data modeling and database management systems), compiler techniques,
web application development, and software security courses. For 15 years, he was
responsible for developing and running web-based programming courses in Java,
which were offered to off-campus students. He has also given numerous courses and
seminars at various levels in object-oriented programming and system development
using Java and Java-related technologies, both at the University of Bergen and vari-
ous other universities in Norway and East Africa, and also for the IT industry.
Vasily A. Strelnikov
Vasily Strelnikov is primarily responsible for developing new review questions for
the chapters contained in this book.
xx PART I: PREFACE
Acknowledgments
At Pearson, senior executive editor Greg Doench was once again in charge of this
project. Senior content producer Julie Nahil was again the in-house contact at Pear-
son, managing the production of the book professionally and efficiently. Freelancer
Deborah Woods did a meticulous job copyediting Part I of this book. Our sincere
thanks to Greg, Julie, Deborah, and all those behind the scenes, who helped to get
this publication out the door.
For the technical review of Part I, we were again lucky to have a Java guru who gra-
ciously agreed to take on the task:
Mikalai Zaikin is a lead Java developer at IBA Lithuania, and is currently located
in Vilnius. He has helped Oracle with development of Java certification exams
and has also been a technical reviewer of several Java certification books. He
also contributes to the Java Quiz column for Oracle’s Java Magazine in collabo-
ration with Simon Roberts.
Without doubt, Mikalai has an eye for detail. It is no exaggeration to say that
his feedback has been invaluable in improving the quality of Part I at all levels.
Our most sincere thanks to Mikalai for the many excellent comments and sug-
gestions on the contents—especially regarding code examples and review
questions, and above all, for weeding out numerous pesky errors in the manu-
script.
Great effort has been made to eliminate mistakes and errors in this book. Any
remaining oversights are solely our responsibility. We hope that our readers will
bring them to our attention when they find them.
Family support was undoubtedly essential in this project as well and for that we
are very grateful to our families for putting up with us.
We wish our reader all the best in going down the caffeine-infused path of Java cer-
tification. May your threads stay untangled and complete normally!
—Khalid A. Mughal
November 5, 2024
Bergen, Norway
This page intentionally left blank
Virtual Threads
3
Chapter Topics
• What virtual threads are and how they compare to platform
threads.
• How virtual threads are executed.
• Creating and using virtual threads.
• Using virtual thread executors to run tasks.
• Best practices for using virtual threads.
Prerequisites
• Understanding how platform threads are executed.
• Understanding the thread lifecycle.
• Creating and using platform threads.
• Using executor services to run tasks.
73
74 CHAPTER 3: VIRTUAL THREADS
on the one-thread-per-task paradigm, allowing each task to execute in its own vir-
tual thread.
Execution of virtual threads is illustrated in Figure 3.1a. As with platform threads,
a virtual thread is first created to run a task and then scheduled to begin execution.
There can be many virtual threads scheduled to begin execution ((1) in
Figure 3.1a). From hereon their execution is at the discretion of the JVM.
In order to run the task in a virtual thread that is ready for execution, the JVM
scheduler assigns the virtual thread to a platform thread for execution—this is called
mounting the virtual thread (vt) and the designated platform thread is called the
carrier thread (ct) ((2) and (5) in Figure 3.1a showing virtual threads vt0, vt12, and
vt11 that are mounted on carrier threads ct1, ct2, and ct3, respectively). A platform
thread is mapped to an OS thread (ost) and acts as a carrier thread when it is exe-
cuting a virtual thread.
4
Transition on unblocked to ready for mounting.
ost1
vt0 ct1
2
Virtual threads ready Unmounted virtual threads
to be mounted for ost2 waiting to become
execution. 1 3 unblocked.
vt12 ct2 [ vt9 , vt4 ,..., vt7 ]
[ vt0 , vt1 ,..., vt2 ]
5 ost3
Unmount
Mount Start Terminated
Executing certain operations in its task can cause a mounted virtual thread to
unmount from its carrier thread—that it, to relinquish its carrier thread (as at (2) in
Figure 3.1a for virtual threads vt0 and vt12). A virtual thread is unmounted when
it executes a blocking operation (such as an I/O operation). It does not require any
action on the part of the application to initiate unmounting when a blocking oper-
ation is executed. I/O operations and other relevant blocking operations in the
APIs have been updated to work with virtual threads. The unmounted virtual
thread remains blocked until its blocking operation is ready to complete ((3) in
Figure 3.1a), at which point, it is unblocked and joins other virtual threads waiting
to mount and thus resume execution ((4) in Figure 3.1a).
A virtual thread that completes its execution while mounted is of course
unmounted and terminated ((5) in Figure 3.1a for virtual thread vt11) and its car-
rier thread used to mount another virtual thread that is ready to be mounted for
execution.
Instead of a carrier thread being monopolized by its virtual thread until the
blocked operation is ready to complete, unmounting it allows the JVM scheduler
to mount another virtual thread that is ready for execution on the carrier thread.
An execution profile of a virtual thread is shown in Figure 3.1b, illustrating that the
virtual thread executes when it is mounted on a carrier thread, and is unmounted
and blocked on a blocking operation until the operation is ready to complete. Note
that on resumption of its execution, a virtual thread may be mounted on the same
carrier thread or on a different carrier thread.
The ratio m:n of m virtual threads to n platform threads is usually very high, contrib-
uting to the high-throughput of the virtual thread execution model. A task
assigned to a platform thread remains assigned to the platform thread throughout
its lifetime—even while it is in a waiting or a blocked state, and cannot therefore
do any useful work. The task in a virtual thread also remains assigned to the same
virtual thread through its lifetime, but the virtual thread may be executed on one
or more platform threads, freeing the carrier thread to do other work when the vir-
tual thread is unmounted and blocked. The virtual thread execution model results
in high utilization of the platform threads by multitude of virtual threads, which
counts for the high throughput of the one-thread-per-task execution model.
Platform threads are also known as classical threads or traditional threads. OS threads
are also known as native threads or kernel threads.
static { // (3)
System.setProperty("java.util.logging.SimpleFormatter.format",
"[%1$tT.%1$tN] %4$s: %5$s%n");
}
A task is defined at (1) as a Runnable that prints the string representation of the cur-
rent thread, which in this case will be a virtual thread, when the task is executed.
Note that the print method is a blocking operation.
Runnable task = () -> System.out.println(Thread.currentThread()); // (1)
The virtual method created and started at (2) will execute the task at (1) when it is
allowed to run, printing information about the virtual thread:
VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
In this case, the string representation of the current thread comprises of the follow-
ing components:
• VirtualThread identifies that it is a virtual thread.
• #21 specifies the unique thread ID of the virtual thread. In this case it is 21. The
thread ID does not change during the lifetime of a thread.
• runnable indicates the state the thread is in. In this case, it is in the runnable state.
• ForkJoinPool-1-worker-1 is composed of the name of the fork-join pool and the
name of the carrier thread on which the virtual thread is mounted. The name
ForkJoinPool-1 identifies the fork-join pool that manages the carrier threads
(which are platform threads). Designation worker-1 is the name of the carrier
thread in the fork-join pool ForkJoinPool-1 on which the virtual thread with ID
#21 is mounted.
The number value in the thread ID designation, the carrier thread designation,
and the fork-join pool designation are counter values giving an indication of
how many of these entities have been created.
Virtual threads are daemon threads—that is, they are unceremoniously terminated
when the parent platform thread terminates. The virtual thread created at (2) in
Example 3.2 can risk being terminated before it has completed if the parent plat-
form thread (in this case, the main thread) completes first. In order to allow the vir-
tual thread to complete its execution, the parent thread can call the join() method
on the virtual thread in order to wait for its completion before proceeding. The call
at (4) will ensure that the main thread will wait indefinitely and does not proceed
before the virtual thread completes its execution.
vt.join(); // (4)
In Example 3.2, the main thread logs information about whether the virtual thread
is alive before and after calling the join() method on the virtual thread. The logged
information shows that virtual thread #21 was alive before the call to the join()
method, but had completed its execution after the call.
Finally, the information logged at (5) in Example 3.2 shows that a virtual thread is
an instance of the java.lang.VirtualThread class. This class is a non-public subclass of
the Thread class in the java.lang package and not accessible outside this package.
For all intents and purposes, it is the Thread class that provides the support for vir-
3.3: USING Thread CLASS TO CREATE VIRTUAL THREADS 79
tual threads. However, the application has to keep track of whether a reference of
type Thread denotes a virtual or a platform thread.
// Create a task:
Runnable task = () -> System.out.println(Thread.currentThread()); // (1)
vt.join(); // (4)
java.lang.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.
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 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 cre-
ated by the start() method of the thread builder will be scheduled to begin execu-
tion, 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 equiva-
lent to the code at (3):
Thread vt1 = vtBuilder.unstarted(task);
Thread vt2 = vtBuilder.unstarted(task);
vt1.start();
82 CHAPTER 3: VIRTUAL THREADS
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 com-
pleted 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 meth-
ods to set various properties of a platform thread.
java.lang.Thread.Builder
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.
// 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());
86 CHAPTER 3: VIRTUAL THREADS
// 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
// 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();
}
}
Thread builders provide the factory() method that returns a thread factory based
on the current state of the builder, such as the thread name to use when creating
threads.
ThreadFactory vtf = Thread.ofVirtual().name("VT_", 1).factory(); // (2)
In the code above from Example 3.5, the virtual thread factory (ThreadFactory)
obtained from the virtual thread builder (Thread.Builder.OfVirtual) that is returned
by the Thread.ofVirtual() method will create unstarted virtual threads whose
names will be VT_1, VT_2, and so on.
Unstarted virtual threads are created at (2) by calling the newThread() method of the
thread factory and have to be explicitly scheduled to begin execution by calling the
start() method of the Thread class:
Thread vt4 = vtf.newThread(task); // VT_1
...
vt4.start();
Using a platform thread factory obtained from a platform thread builder is analo-
gous to using a virtual thread factory obtained from a virtual thread builder.
vt4.start();
vt5.start();
vt4.join();
vt5.join();
}
}
VirtualThread[#20,vt_4]/runnable@ForkJoinPool-1-worker-1: I am on it!
java.util.concurrent.ThreadFactory
Thread newThread(Runnable r)
Constructs a new unstarted Thread to run the given runnable.
Returns the constructed thread, or null if the request to create a thread is
rejected.
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.
// 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));
}
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!
java.util.concurrent.Executors
For a virtual thread, it returns pertinent information about the virtual thread in the
following format:
VirtualThread[#22,VT_2]/runnable@ForkJoinPool-1-worker-2
3.7: SCALABILITY OF THROUGHPUT WITH VIRTUAL THREADS 91
From the string representation, we can see that the virtual thread with ID #22 is
mounted on carrier thread worker-2. We can extract the name of carrier thread with
this code:
String ctName1 = vtInfo1.substring(vtInfo1.indexOf('w')); // worker-2
If virtual thread #22 executes a blocking operation, it will be unmounted and when
it resumes execution, it might be mounted on a different or the same carrier thread.
We can check this from the string representation of the virtual thread after resump-
tion:
String vtInfo2 = Thread.currentThread().toString();
If the string representation of the virtual thread is as below, we know that it was
mounted on carrier thread worker-4.
VirtualThread[#22,VT_2]/runnable@ForkJoinPool-1-worker-4
We can graphically represent the scheduling of a virtual thread from one carrier
thread to another after a blocking operation as follows:
worker-2 -> worker-4
The getCarrierThreadName() static method at (7) in Example 3.7 extracts the name of
the carrier thread the virtual thread is mounted on as outlined above. We call the
getCarrierThreadName() method to extract the name of the carrier thread before and
after each blocking operation in the task defined at (3):
String ctName1 = getCarrierThreadName(); // Carrier thread before.
someBlockingOperation();
String ctName2 = getCarrierThreadName(); // Carrier thread after.
...
The scheduling of a virtual thread from one carrier thread to another after each
blocking operation is printed in the same order as the blocking operations. In order
to keep the output manageable, only execution of a limited number of virtual
threads is printed (controlled by the INTERVAL value defined at (2)).
A sleeping operation with a duration of 1 second is implemented as the blocking
operation by the method someBlockingOperation() declared at (8).
The main() method at (9) uses a one-virtual-thread-per-task executor service to sub-
mit and execute the task a fixed number of times (NUM_OF_VT defined at (1)). The
main() method also computes the time the executor service used to execute the sub-
mitted tasks (i.e., the duration) at (10) and the throughput (i.e., number of tasks/dura-
tion) at (11).
From the output in Example 3.7, we can see the carrier threads that a virtual thread
was mounted on to execute the task. At the resumption of execution after blocking,
92 CHAPTER 3: VIRTUAL THREADS
a virtual thread can be mounted on the same or a different carrier thread. Virtual
thread #200000 was mounted consecutively on the same carrier thread twice:
Virtual Thread #200000: worker-2 -> worker-2 -> worker-5 -> worker-5
Whereas, virtual thread #300000 was mounted on different carrier threads after
blocking during its lifetime.
Virtual Thread #300000: worker-3 -> worker-8 -> worker-4 -> worker-5
From the output in Example 3.7, we can see that the number of carrier threads
employed by the executor service is 8 (the highest count on a carrier thread name
in the output that is the same as the number of processors in this case).
Example 3.7 is running a million virtual threads (with a one-thread-per-task exec-
utor service taking a little over 54 seconds). The very high ratio of virtual threads
executed to carrier threads employed to execute them results in formidable scaling
of throughput, in this case a little over 18000 tasks/second. The curious reader is
encouraged to experiment with different values for the number of tasks to execute,
and to refactor the code to use platform threads and different executor services.
An important factor to keep in mind is that virtual threads can help to increase the
throughput under the right circumstances, but they do not improve the latency—
that is, they do not make each task execute faster.
// Print carrier threads the virtual thread was mounted on: (6)
if (vtID % INTERVAL == 0) {
System.out.printf("Virtual Thread #%d: %s -> %s -> %s -> %s%n",
vtID, ctName1, ctName2, ctName3, ctName4);
}
};
Virtual Thread #1000000: worker-5 -> worker-5 -> worker-5 -> worker-7
Number of virtual threads: 1000000
Duration: 54407 ms
Throughput: 18379.99 tasks/s
it is frequent and long-lived. Note that a pinned virtual thread does not block its
associated carrier thread unless a blocking operation is executed. If that happens,
the associated carrier thread remains idle when blocked, further increasing the
impact of pinning.
Example 3.8 illustrates both pinning of virtual threads in a synchronized block and
how refactoring the code to use a reentrant lock can alleviate the problem. The
example prints the schedule trace of carrier threads on which a virtual thread is
mounted during the execution of its task.
In Example 3.8, a blocking operation is defined by the method blockingOp() at (3).
The method returns a string of the form "worker-n -> worker-m" that identifies the
scheduling of carrier threads the virtual thread was mounted on before and after the
blocking operation, respectively.
Example 3.8 is run with the above flag having the value short for tracing pinned
threads. The scheduling trace of the carrier threads is printed at (10). For example,
we see the following output for virtual thread #28:
Thread[#35,ForkJoinPool-1-worker-7,5,CarrierThreads]
vt.VTPinningDemo.lambda$0(VTPinningDemo.java:41) <== monitors:1
[10:27:31] INFO: vt #28: LockAcquiring(worker-7 -> worker-7) ->
BlockingOp(worker-7 -> worker-7) -> worker-7
The first two lines above show that carrier thread #35, having the name worker-7,
was blocked during the execution of the blocking operation in the synchronized
block. From the output we can see that virtual thread #28 is pinned to carrier thread
#35 with the name worker-7 that is blocked.
The last two lines show that virtual thread #28 was pinned to carrier thread worker-
7 during the entire execution of the synchronized block: when acquiring the lock
of the synchronized object, during the blocking operation, and after the synchro-
nized block. It is important to note that not only was the virtual thread pinned to
96 CHAPTER 3: VIRTUAL THREADS
its carrier thread, but the carrier thread was also blocked during the blocking oper-
ation. Pinning not only takes the associated carrier thread out of scheduling for
other virtual threads, but during a blocking operation, it is also idle for the dura-
tion of the blocking period.
Similarly, pinning of the virtual threads can be traced in all runs of the task con-
taining the synchronized block. Note that each virtual thread is assigned to a new
carrier thread as virtual threads get pinned executing the synchronized block.
// Blocking operation:
private static String blockingOp() { // (3)
try {
var ctNameBefore = getCarrierThreadName();
TimeUnit.MILLISECONDS.sleep(DURATION);
var ctNameAfter = getCarrierThreadName();
return String.format("%s -> %s", ctNameBefore, ctNameAfter);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "? -> ?";
}
logger.info(String.format( // (10)
"vt %4s: LockAcquiring(%s -> %s) -> BlockingOp(%s) -> %s",
vtID(), ctBeforeLock, ctAfterLock, blockTrace, ctAfterSynch));
};
// Reentrant lock:
public static final ReentrantLock lock = new ReentrantLock(); // (11)
logger.info(String.format( // (18)
"vt %4s: LockAcquiring(%s -> %s) -> BlockingOp(%s) -> %s",
vtID(), ctBeforeLock, ctAfterLock, blockTrace, ctAfterUnlock
));
};
logger.info("-----Reentrant lock-----");
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, NUMBER_OF_VT).forEach(i -> executor.submit(task2));
}
}
Thread[#35,ForkJoinPool-1-worker-7,5,CarrierThreads]
vt.VTPinningDemo.lambda$0(VTPinningDemo.java:41) <== monitors:1
[10:27:31] INFO: vt #28: LockAcquiring(worker-7 -> worker-7) ->
BlockingOp(worker-7 -> worker-7) -> worker-7
[10:27:32] INFO: vt #23: LockAcquiring(worker-2 -> worker-2) ->
BlockingOp(worker-2 -> worker-2) -> worker-2
[10:27:33] INFO: vt #29: LockAcquiring(worker-8 -> worker-8) ->
BlockingOp(worker-8 -> worker-8) -> worker-8
[10:27:35] INFO: vt #26: LockAcquiring(worker-5 -> worker-5) ->
BlockingOp(worker-5 -> worker-5) -> worker-5
[10:27:36] INFO: vt #21: LockAcquiring(worker-1 -> worker-1) ->
BlockingOp(worker-1 -> worker-1) -> worker-1
[10:27:37] INFO: vt #27: LockAcquiring(worker-6 -> worker-6) ->
BlockingOp(worker-6 -> worker-6) -> worker-6
[10:27:38] INFO: vt #24: LockAcquiring(worker-3 -> worker-3) ->
BlockingOp(worker-3 -> worker-3) -> worker-3
[10:27:39] INFO: vt #25: LockAcquiring(worker-4 -> worker-4) ->
BlockingOp(worker-4 -> worker-4) -> worker-4
[10:27:39] INFO: -----Reentrant lock-----
[10:27:40] INFO: vt #38: LockAcquiring(worker-4 -> worker-4) ->
BlockingOp(worker-4 -> worker-1) -> worker-1
[10:27:41] INFO: vt #39: LockAcquiring(worker-3 -> worker-7) ->
BlockingOp(worker-7 -> worker-1) -> worker-1
[10:27:42] INFO: vt #41: LockAcquiring(worker-1 -> worker-7) ->
BlockingOp(worker-7 -> worker-1) -> worker-1
[10:27:43] INFO: vt #40: LockAcquiring(worker-6 -> worker-7) ->
BlockingOp(worker-7 -> worker-1) -> worker-1
[10:27:44] INFO: vt #42: LockAcquiring(worker-5 -> worker-7) ->
BlockingOp(worker-7 -> worker-1) -> worker-1
[10:27:45] INFO: vt #43: LockAcquiring(worker-8 -> worker-7) ->
BlockingOp(worker-7 -> worker-1) -> worker-1
[10:27:46] INFO: vt #44: LockAcquiring(worker-2 -> worker-7) ->
BlockingOp(worker-7 -> worker-1) -> worker-1
[10:27:47] INFO: vt #45: LockAcquiring(worker-7 -> worker-7) ->
BlockingOp(worker-7 -> worker-1) -> worker-1
The method lock() of the ReentrantLock class is a blocking operation. Other threads
will wait if the lock is already taken. Especially if a virtual thread gets blocked
because the lock is taken, the virtual thread is unmounted and its carrier thread can
3.8: BEST PRACTICES FOR USING VIRTUAL THREADS 99
be scheduled to service other virtual threads by the JVM thread scheduler. There is
no pinning involved.
As before, the code in the task traces the carrier threads that the virtual thread was
mounted at various points in the code: before obtaining the lock at (13), after
obtaining the lock at (15), executing the blocking operation at (16), and after the
lock is freed at (17).
The scheduling trace of the carrier threads is printed at (18). For example, we see
the following output for virtual thread #38:
[10:27:40] INFO: vt #38: LockAcquiring(worker-4 -> worker-4) ->
BlockingOp(worker-4 -> worker-1) -> worker-1
The trace for acquiring the lock shows that virtual thread #38 was mounted on car-
rier thread worker-4, most probably acquired the lock straight away, since the trace
shows the same carrier thread before and after acquiring the lock.
The trace for the blocking operation shows that virtual thread #38 was mounted on
carrier thread worker-4 before the blocking operation and was mounted on carrier
thread worker-1 when it was allowed to resume execution after blocking. While it
was blocked, its carrier thread worker-4 can be scheduled to execute other virtual
threads. Again, there is no pinning during the blocking operation.
The unlock() method of the ReentrantLock class is not a blocking operation. The
scheduling trace shows that virtual thread #38 continued execution while mounted
on carrier thread worker-1.
Similarly, the scheduling traces of the other virtual threads show that there is no
pinning when using a reentrant lock in other runs of the task. Note that since the
virtual threads were not pinned, their associated carrier threads can be scheduled
to service other virtual threads waiting to execute, as evident from the runs where
the same carrier thread was involved in the execution of several other virtual
threads.
Review Questions
(b) Program will log the value of i and continue to run indefinitely.
(c) Program will log nothing and terminate.
(d) Program will log one or more values of i and terminate.
};
Thread t1 = Thread.ofVirtual().name("acme").unstarted(r1);
t1.start();
t1.interrupt();
}
}
(d) An unmounted virtual thread when mounted to resume execution will con-
tinue running on the same platform thread.
Which options will cause the program to execute normally and always print NAME:
vt_1 when inserted at (1)?
Select the two correct answers.
(a) Thread vt = Thread.startVirtualThread(task);
vt.setName("vt_1");
vt.start();
vt.join();
(b) Thread vt = Thread.startVirtualThread(task);
vt.setName("vt_1");
vt.join();
(c) Thread.Builder.OfVirtual vtb = Thread.ofVirtual().name("vt_", 1);
Thread vt = vtb.unstarted(task);
vt = vtb.start(task);
vt.join();
(d) Thread.Builder.OfVirtual vtb = Thread.ofVirtual();
Thread vt = vtb.name("vt_", 1).started(task);
vt.join();
(e) Thread.Builder.OfVirtual vtb = Thread.ofVirtual();
Thread vt = vtb.unstarted(task).name("vt_", 1);
vt.join();
(f) Thread.Builder.OfVirtual vtb = Thread.ofVirtual().name("vt_", 1);
Thread vt = vtb.unstarted(task);
vt.start(task);
vt.join();
(g) Thread.Builder.OfVirtual vtb = Thread.ofVirtual().name("vt_", 1);
Thread vt = vtb.unstarted(task);
vt = vt.start();
vt.join();
(h) Thread vt = Thread.ofVirtual().name("vt_", 1).start(task);
vt.join();
(i) Thread.ofVirtual().name("vt_", 1).start(task).join();
Which options will cause the program to execute normally and print NAME: vt_1
when inserted at (1)?
Select the two correct answers.
(a) ThreadFactory vtf = Thread.ofVirtual().name("vt_0").factory();
Thread vt = vtf.newThread(task);
vt.setName("vt_1");
vt.start();
vt.join();
(b) ThreadFactory vtf = Thread.ofVirtual().name("vt_0").factory();
Thread vt = new Thread(task);
vt.setName("vt_1");
vt.start();
vt.join();
(c) Thread vt = Thread.ofVirtual().name("vt_1").factory().newThread(task);
vt.join();
(d) Thread vt = Thread.ofVirtual().name("vt_1").factory().newThread(task);
vt.start().join();
(e) Thread.Builder.OfVirtual vtb = Thread.ofVirtual().name("vt_", 1);
vtb.unstarted(task);
Thread vt = vtb.factory().newThread(task);
vt.start();
vt.join();
This page intentionally left blank
Part I: Index
Symbols ConcurrentSkipListMap 69
sequenced methods 69
: 11 ConcurrentSkipListSet 66, 67
-> 11 conditional and operator (&&)
pattern variable 9
A conditional or operator (||)
pattern variable 10
ArrayDeque 56 context switching 74
CopyOnWriteArrayList 66, 67
core map interfaces 47, 57
B CPU-bound tasks 99
blocking operations 76, 78 critical region 95, 98
BlockingDeque 66
BlockingQueue 66
D
daemon threads 78
C default label 13
carrier threads defined encounter order 46
executing virtual threads 76 insertion order 50
fork-join pool 78 sort order 50
name 78 Deque 49
case pattern 13 deques 52
case pattern labels ArrayDeque 56
exhaustiveness 18 Deque 49
classical threads LinkedList 56
see platform threads
codepoint 123
Collection 48
E
collections enhanced switch construct 11
inheritance hierarchy 47 case label dominance 16, 28
component hierarchy 21 case pattern 13
concurrent applications 74 default label 13
throughput 74 execution 14
ConcurrentLinkedDeque 66, 67 exhaustiveness 20
ConcurrentMap 69 fall-through 20
ConcurrentNavigableMap 69 generic record patterns 30
127
128 PART I: INDEX
M properties 8
scope 4
map entry shadow a field 8
unmodifiable copy 58 type inference with var 23, 27
mappings 56 patterns
see entries 56 context 34
maps 57 record patterns 20
entries 56 syntax 34
inheritance hierarchy 57 type pattern 3
keys 56 platform thread builders
mappings 56 set misc. properties for platform threads
values 56 84
MatchException 35 set name property for platform threads
multi-way branch 11 84
set name property for platform threads
N that uses a counter 84
platform thread factory 87
native threads platform threads 74
see OS threads carrier thread 75
navigable map 57 comparison with virtual threads 84
navigable sets 52 create using platform thread builders 80
NavigableMap 61 create using platform thread factory 87
NavigableSet 49, 54 in executor services 88
NoSuchElementException 50 naming using counter 81
the main thread 78
O
one-thread-per-task executor service
Q
89 qualified enum constants 35
customizing 89 Queue 49
one-thread-per-task paradigm 74
one-virtual-thread-per-task executor
service 88 R
open range 121 record deconstruction 20
operating system (OS) threads 74 record pattern matching
operators enhanced switch construct 26
instanceof pattern match operator 2, 3 instanceof pattern match operator 22
instanceof type comparison operator 2 record patterns 20
generic record patterns 24, 30
P guarded record patterns 27
nested record patterns 23, 26
pattern label 13 syntax 22
pattern match operator 2, 3 using sealed types 29
pattern matching records
MatchException 35 component hierarchy 21
record pattern matching 22, 26 reentrant lock
type pattern matching 2, 11 avoid pinning 98
unexpected failure 35 critical region 98
pattern variable 3 reifiable types 7
cannot shadow local variable 8 reverse-ordered view 50, 51
declare final 8 create 50
flow sensitive scope 8, 18, 23, 27 runnable state 78
130 PART I: INDEX