Virtual Threads & Structured Concurrency practice questions

From OCP Java SE 21 (1Z0-830) · 17 questions on this topic

Virtual Threads & Structured Concurrency practice questions from OCP Java SE 21 (1Z0-830). This pack has 17 questions tagged Virtual Threads & Structured Concurrency, drawn from its timed mock exams. 8 of them are worked through in full below — the question, every option, why each is right or wrong, and the explanation.

Worked examples for Virtual Threads & Structured Concurrency

  1. Question 1

    Which two statements about virtual threads in Java 21 are correct? (Choose two.)

    1. A. Calling setPriority(Thread.MAX_PRIORITY) on a virtual thread has no effect; getPriority() still returns 5Correct answer

      Virtual threads are scheduled by a FIFO ForkJoinPool with no notion of Java thread priority, so setPriority is specified to do nothing and getPriority() always returns NORM_PRIORITY (5).

    2. B. A virtual thread's isVirtual() returns false until the thread is first mounted on a carrier thread

      Confuses being a virtual thread with being mounted. Virtual-ness is a property of the object from construction; an unstarted virtual thread in state NEW already reports isVirtual() == true.

    3. C. A class that extends Thread is always a platform thread; there is no public virtual Thread subclass you can extendCorrect answer

      The implementation class java.lang.VirtualThread is package-private and final, and every public Thread constructor creates a platform thread; virtual threads come only from ofVirtual(), startVirtualThread(...), a virtual ThreadFactory, or newVirtualThreadPerTaskExecutor().

    4. D. Calling Thread.sleep(...) inside a virtual thread blocks its carrier platform thread for the whole sleep duration

      Inverts the feature's central point. Thread.sleep is a blocking operation that UNMOUNTS the virtual thread, releasing the carrier to run other virtual threads — which is exactly why blocking is cheap.

    Explanation

    Why `Calling setPriority(Thread.MAX_PRIORITY) on a virtual thread has...` is correct: virtual threads are scheduled by a FIFO ForkJoinPool scheduler that has no notion of Java thread priority, so `Thread.setPriority` is specified to do nothing on a virtual thread and `getPriority()` always answers `Thread.NORM_PRIORITY`, which is `5`. Compare a platform thread, where the same call really does move the value to `10`. Why `A class that extends Thread is always a platform thread; there is...` is correct: the implementation class is `java.lang.VirtualThread`, which is package-private and final, and every public `Thread` constructor creates a platform thread. So `new Thread(r) { }` reports `isVirtual() == false`. Virtual threads can only be obtained from `Thread.ofVirtual()`, `Thread.startVirtualThread(...)`, a virtual `ThreadFactory`, or `Executors.newVirtualThreadPerTaskExecutor()` — never by subclassing. Why the others are wrong: `A virtual thread's isVirtual() returns false until the thread is first...` confuses *being a virtual thread* with *being mounted*. Virtual-ness is a property of the object from construction: an unstarted virtual thread in state `NEW` already reports `isVirtual() == true`. Mounting is a scheduling event, not an identity change. `Calling Thread.sleep(...) inside a virtual thread blocks its carrier...` inverts the central point of the feature. `Thread.sleep` is one of the blocking operations that *unmounts* a virtual thread, releasing the carrier to run other virtual threads; that is exactly why blocking is cheap. The belief it encodes — that a virtual thread holds its carrier while blocked — would make virtual threads no better than a fixed pool. Exam tip: virtual threads deliberately drop the platform-thread knobs. Priority is ignored, `setDaemon(false)` is rejected (they are always daemon), thread groups are fixed, and `Thread.Builder.OfVirtual` offers no `priority()`, `stackSize()`, `group()` or `daemon()` method at all — those live only on `Thread.Builder.OfPlatform`.

  2. Question 2

    One platform thread and one virtual thread are started and joined in turn. What does this print? ```java public class Main { public static void main(String[] args) throws InterruptedException { Thread p = Thread.ofPlatform().start(() -> System.out.print("P")); p.join(); Thread v = Thread.ofVirtual().start(() -> System.out.print("V")); v.join(); System.out.print(" " + p.isVirtual() + " " + v.isVirtual()); } } ```

    1. A. PV false trueCorrect answer

      p.join() forces the platform thread's P before the virtual thread is even created, fixing the order PV; isVirtual() then reports false for the ofPlatform() thread (an OS thread) and true for the ofVirtual() one.

    2. B. PV true false

      Inverts which builder yields which kind of thread. ofPlatform() builds an OS-backed platform thread (false), and ofVirtual() builds a virtual thread (true).

    3. C. VP false true

      Assumes the lightweight virtual thread overtakes and prints first, but p.join() makes main wait for the platform thread, and v is not even started until p has terminated, so the order is PV.

    4. D. PV true true

      Believes the Thread.Builder API creates virtual threads either way. ofPlatform() builds the same kind of platform thread as the constructor, so its isVirtual() is false.

    Explanation

    Trace: `Thread.ofPlatform()` and `Thread.ofVirtual()` are the two builder entry points added in Java 21, and they build different kinds of thread. The platform thread prints `P`, and `p.join()` forces main to wait for it before the virtual thread is even created, so the order `PV` is fixed rather than left to the scheduler. `isVirtual()` then reports what each object *is*: `false` for the `ofPlatform()` thread (a 1:1 wrapper over an OS thread) and `true` for the `ofVirtual()` one. Result: `PV false true`. Why the others are wrong: `PV true true` encodes the belief that the `Thread.Builder` API creates virtual threads either way — that `Thread.ofPlatform()` is just a modern spelling of `new Thread(...)` that has been quietly upgraded. It is not: it builds exactly the same kind of platform thread as the constructor. `VP false true` assumes the lightweight virtual thread somehow overtakes and prints first; the intervening `p.join()` makes that impossible, and in any case `v` is not started until `p` has terminated. `PV true false` simply inverts which builder yields which kind of thread. Exam tip: `isVirtual()` is an identity check, fixed at construction — it does not depend on whether the thread is started, mounted, or finished. And remember which factory is which: `ofPlatform()` → OS thread, not a daemon by default; `ofVirtual()` → virtual thread, always a daemon.

  3. Question 3

    A developer wants the virtual thread to be a named daemon thread and writes the builder chain below. What is the result? ```java public class Main { public static void main(String[] args) throws InterruptedException { Thread t = Thread.ofVirtual() .name("worker") .daemon(true) .start(() -> System.out.print("running")); t.join(); System.out.print(" " + t.isDaemon()); } } ```

    1. A. running true

      What you would get if daemon(boolean) were inherited from a common builder interface. It names the right values (virtual threads ARE daemons) but daemon() exists only on OfPlatform, so the code never compiles.

    2. B. Compilation fails: Thread.Builder.OfVirtual has no daemon(boolean) methodCorrect answer

      ofVirtual() returns Thread.Builder.OfVirtual; daemon(boolean) exists only on OfPlatform because a virtual thread is always a daemon, so `.daemon(true)` on the OfVirtual chain fails to compile with "cannot find symbol".

    3. C. running false

      Compounds the inheritance error with the belief that daemon(true) could be overridden to leave a non-daemon virtual thread. Virtual threads can never be non-daemon, and the method does not exist anyway, so the code does not compile.

    4. D. It compiles, but start(...) throws UnsupportedOperationException at runtime

      Assumes the restriction is enforced at runtime. The builder simply does not offer daemon(boolean), so the type system rejects the call at compile time, not at runtime.

    Explanation

    Trace: `Thread.ofVirtual()` returns a `Thread.Builder.OfVirtual`. The `Thread.Builder` hierarchy splits deliberately: the shared `Thread.Builder` interface carries only what both kinds of thread support (`name`, `inheritInheritableThreadLocals`, `uncaughtExceptionHandler`, `unstarted`, `start`, `factory`), while `daemon(boolean)`, `priority(int)`, `stackSize(long)` and `group(...)` exist **only** on `Thread.Builder.OfPlatform`. A virtual thread is always a daemon, so there is nothing to configure — and the API expresses that by simply not offering the method. `.name("worker")` returns `OfVirtual`, so `.daemon(true)` is resolved against `OfVirtual` and no such method exists. javac reports `cannot find symbol: method daemon(boolean), location: interface OfVirtual`, so nothing runs. Why the others are wrong: `running true` is what you would get if `daemon(boolean)` were inherited from a common builder interface — it names the right *values* (virtual threads are indeed daemons) but the code never reaches runtime to print them. `running false` compounds that with the belief that `daemon(true)` could be overridden or ignored, leaving a non-daemon virtual thread; virtual threads can never be non-daemon. `It compiles, but start(...) throws UnsupportedOperationException...` assumes the restriction is enforced at runtime rather than by the type system — a reasonable guess, but the builder rejects it at compile time. Exam tip: read the *static* type the builder chain currently has. `ofVirtual()` gives you `OfVirtual`; `ofPlatform()` gives you `OfPlatform`; only the platform builder exposes the OS-thread knobs. The related runtime trap: `Thread.setDaemon(false)` on an already-created virtual thread compiles fine and then throws `IllegalArgumentException` at runtime — the same idea, caught at a different stage.

  4. Question 4

    Which factory creates an ExecutorService that starts a new virtual thread for every submitted task?

    1. A. Executors.newCachedThreadPool() always uses virtual threads in Java 21

      newCachedThreadPool() creates platform threads in Java 21; no pre-existing factory silently switched to virtual threads.

    2. B. Executors.newVirtualThreadPool(n)

      No such method exists; the plausible-sounding name is bait. Pooling virtual threads is exactly what the design avoids — they are cheap enough to create per task.

    3. C. Executors.newVirtualThreadPerTaskExecutor()Correct answer

      This factory returns an ExecutorService that creates a brand-new virtual thread for every submitted task — no pooling and no queueing behind a bounded worker count (JEP 444).

    4. D. Executors.newFixedThreadPool(0)

      newFixedThreadPool(0) throws IllegalArgumentException (the thread count must be positive), and a fixed pool uses platform threads anyway.

    Explanation

    The virtual-thread-per-task executor is the factory that spawns a fresh virtual thread for each submitted task rather than reusing a bounded set of workers. Virtual threads are cheap enough to create per task, so any choice that hands them a pool size should raise suspicion — they are created per task and never pooled. The pre-existing pool factories still produce platform threads in Java 21, and a fixed pool of size zero is not even legal.

  5. Question 5

    What does this print? ```java public class Main { public static void main(String[] args) throws InterruptedException { Thread t = Thread.startVirtualThread( () -> System.out.print(Thread.currentThread().isVirtual())); t.join(); } } ```

    1. A. false

      The task runs on the virtual thread it was started on, not on main, so isVirtual() is not false.

    2. B. Compilation fails: startVirtualThread is not a member of Thread

      startVirtualThread is a real static method on Thread, final in Java 21, so the code compiles.

    3. C. Throws IllegalStateException

      Nothing here violates thread lifecycle rules, so no exception is thrown.

    4. D. trueCorrect answer

      Thread.startVirtualThread(r) creates and starts a virtual thread; inside the task Thread.currentThread() is that virtual thread, so isVirtual() prints true, and join() makes main wait for it.

    Explanation

    `Thread.startVirtualThread(r)` is the one-line shortcut that both creates and starts a virtual thread. Inside the task, `Thread.currentThread()` is that virtual thread, so `isVirtual()` reports true, and `join()` makes the main thread wait for it to finish. It is one of three equivalent creation idioms alongside `Thread.ofVirtual().start(r)` and `Executors.newVirtualThreadPerTaskExecutor()`, which the exam mixes freely.

  6. Question 6

    A ThreadFactory is obtained from a virtual thread builder and asked for a new thread. What does this print? ```java import java.util.concurrent.ThreadFactory; public class Main { public static void main(String[] args) throws InterruptedException { ThreadFactory factory = Thread.ofVirtual().factory(); Thread t = factory.newThread(() -> System.out.print("task ")); System.out.print(t.getState() + " "); t.start(); t.join(); System.out.print(t.isVirtual()); } } ```

    1. A. task NEW true

      Assumes newThread(...) starts the task immediately, like Thread.Builder.start(...). newThread only CREATES the thread, so getState() reads NEW before it has run.

    2. B. NEW task trueCorrect answer

      ThreadFactory.newThread only creates a thread, so getState() is NEW; after start() the virtual thread prints "task" and join() orders it before main; isVirtual() is true because the factory came from ofVirtual().

    3. C. NEW task false

      Assumes a ThreadFactory hands back platform threads. The factory inherits its kind from the ofVirtual() builder, so the thread is virtual and isVirtual() is true.

    4. D. RUNNABLE task true

      Assumes a factory-created thread is already alive and schedulable. A thread only leaves NEW when start() is called, so getState() reads NEW, not RUNNABLE.

    Explanation

    Trace: `Thread.ofVirtual().factory()` returns a `ThreadFactory` that manufactures virtual threads, but `ThreadFactory.newThread(Runnable)` only *creates* a thread — it never starts one. So at the moment `getState()` is called the thread has not run, and it reports `NEW`. Main prints `NEW `, then `start()` schedules the virtual thread, which prints `task `, and `join()` guarantees that write lands before main resumes. Finally `t.isVirtual()` is `true`, because the factory came from `ofVirtual()` — the thread's runtime class really is `java.lang.VirtualThread`. Result: `NEW task true`. Why the others are wrong: `task NEW true` encodes the belief that `newThread(...)` starts the task immediately, like `Thread.Builder.start(...)` does; it would then have run before `getState()` was read. `RUNNABLE task true` assumes a factory-created thread is already alive and schedulable; a thread only leaves `NEW` when `start()` is called. `NEW task false` assumes a `ThreadFactory` hands back ordinary platform threads — but the factory inherits its kind from the builder it was derived from, so `isVirtual()` is `true`. Exam tip: `Thread.Builder` gives you three exits, and only one of them runs anything: `start(r)` starts a thread, `unstarted(r)` returns a `NEW` one, and `factory()` returns a `ThreadFactory` whose `newThread(r)` is also unstarted. The reverse trap is `Thread.startVirtualThread(r)`, which *does* start immediately.

  7. Question 7

    What does this print? ```java public class Main { public static void main(String[] args) throws InterruptedException { Thread.Builder b = Thread.ofVirtual().name("worker-", 0); Thread t1 = b.start(() -> { }); Thread t2 = b.start(() -> { }); t1.join(); t2.join(); System.out.print(t1.getName() + " " + t2.getName()); } } ```

    1. A. worker-0 worker-1Correct answer

      name("worker-", 0) gives an incrementing counter starting at 0, so the first start() yields worker-0 and the second worker-1; the builder is reusable and keeps counting.

    2. B. worker- worker-

      The two-argument name(prefix, start) appends the counter; only the one-argument name(String) gives every thread the identical fixed name.

    3. C. worker-1 worker-2

      The counter starts at the given value 0, not at 1.

    4. D. Compilation fails: a Thread.Builder cannot be reused

      Reusing a builder is legal and is exactly how you create several similarly-named threads.

    Explanation

    The two-argument `name(prefix, start)` form attaches an incrementing counter beginning at the supplied start value, so successive threads get the prefix followed by 0, then 1, and so on. A Thread.Builder is reusable and retains that counter across calls, so building two threads yields the prefix with 0 then 1. This differs from the single-argument `name(String)` form, which assigns every thread the same fixed name — a favorite subtlety, so check whether a numeric second argument is passed.

  8. Question 8

    Which is the recommended use of virtual threads?

    1. A. Using them for CPU-bound parallel computation to beat the fork/join pool

      Virtual threads add no CPU capacity: they multiplex over the same cores. CPU-bound parallelism belongs to the fork/join pool and parallel streams.

    2. B. One virtual thread per task for I/O-bound, blocking-style code, avoiding shared poolingCorrect answer

      The intended model is one virtual thread per task for I/O-bound, blocking-style workloads (server requests, fan-out calls). Virtual threads are cheap to create, so you spawn one per task and discard it instead of sharing a pool.

    3. C. Replacing all platform threads including the carriers

      The carriers must be platform threads; virtual threads run on top of them and cannot replace them.

    4. D. Pooling a small fixed number of virtual threads like platform threads

      Pooling a small fixed number of virtual threads recreates exactly the bottleneck virtual threads were designed to remove.

    Explanation

    Virtual threads are designed for I/O-bound, blocking-style workloads using one cheap thread per task rather than a shared pool. They improve throughput and scalability of blocking I/O, not the speed of computation — they multiplex over the same cores, so CPU-bound parallelism still belongs to the fork/join pool and parallel streams. Their carriers remain platform threads, and pooling a fixed number of them defeats the whole purpose. Treat 'virtual threads make code run faster' as always wrong.

Practise all 17 Virtual Threads & Structured Concurrency questions

OCP Java SE 21 has the full set, inside timed mock exams that mirror real exam conditions — every question with a worked explanation.

Open OCP Java SE 21

Other topics in this pack