Virtual Threads & Structured Concurrency practice questions

From OCP Java SE 25 (1Z0-831) · 17 questions on this topic

Virtual Threads & Structured Concurrency practice questions from OCP Java SE 25 (1Z0-831). 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

    A virtual thread is built without calling `.name(...)`. What does this print? ```java public class Main { public static void main(String[] args) { Thread v = Thread.ofVirtual().unstarted(() -> {}); System.out.print("[" + v.getName() + "]"); } } ```

    1. A. [VirtualThread-0]

      VirtualThread-0 resembles the synthetic id shown by a virtual thread's toString(), but getName() of an unnamed thread is "", not that id.

    2. B. [Thread-0]

      Thread-N is the auto-generated name pattern for platform threads created with new Thread(); the Builder API does not apply it.

    3. C. []Correct answer

      A virtual thread built without a name has the empty string as its name, so getName() returns "" and the brackets wrap nothing.

    4. D. [main]

      main is the name of the main thread, not of the newly built virtual thread.

    Explanation

    Unless .name(...) is called on the builder, a virtual thread's name is the empty string, so getName() returns "" and printing it between brackets yields []. The synthetic identifier a virtual thread shows in toString() is not its name, and the name stays empty even after the thread starts.

  2. Question 2

    Which TWO statements about virtual threads on Java 25 are correct? (Choose two.)

    1. A. Calling setDaemon(false) on a virtual thread turns it into a non-daemon thread that keeps the JVM alive.

      setDaemon(false) on a virtual thread does not create a non-daemon thread; it throws IllegalArgumentException, and virtual threads can never keep the JVM alive.

    2. B. A virtual thread is always a daemon thread, so on its own it will not keep the JVM alive after main() returns.Correct answer

      Every virtual thread is created as a daemon thread whose daemon status cannot be changed, so once only virtual threads remain the program exits and they do not hold the JVM open.

    3. C. Blocking inside a synchronized block still pins the carrier on Java 25, so a ReentrantLock is mandatory to avoid pinning.

      The premise is outdated: since JEP 491 synchronized no longer pins, so a ReentrantLock is not required merely to avoid pinning (it remains useful for fairness or tryLock).

    4. D. Since JDK 24 (JEP 491), a virtual thread that blocks inside a synchronized block generally no longer pins its carrier.Correct answer

      JEP 491, delivered in JDK 24, removed the limitation that blocking inside a synchronized block or method pinned the carrier, so on Java 25 such blocking generally unmounts normally.

    Explanation

    Two Java 24/25 facts combine here: virtual threads are always daemon threads and so cannot keep the JVM alive after main returns, and JEP 491 (delivered in JDK 24) eliminated the old rule that blocking inside synchronized pinned the carrier. The remaining choices recycle the pre-24 "synchronized pins" belief and the false assumption that setDaemon(false) can make a virtual thread non-daemon.

  3. Question 3

    What does this print? ```java public class Main { public static void main(String[] args) { var factory = Thread.ofVirtual().name("svc-", 10).factory(); Thread t1 = factory.newThread(() -> {}); Thread t2 = factory.newThread(() -> {}); System.out.print(t1.getName() + " " + t2.getName()); } } ```

    1. A. svc-10 svc-11Correct answer

      name("svc-", 10) sets an auto-incrementing counter starting at 10, and factory() returns a ThreadFactory holding that counter, so the first thread is svc-10 and the second svc-11.

    2. B. svc-10 svc-10

      Assumes the counter is fixed, but the two-argument name(prefix, start) increments for every new thread.

    3. C. svc-0 svc-1

      Ignores the start value 10; counting begins at the supplied number, not 0.

    4. D. Compilation fails: factory() is not a method on Thread.Builder

      Thread.Builder.factory() is a real method returning a ThreadFactory, so the code compiles.

    Explanation

    The two-argument name(prefix, start) attaches an auto-incrementing counter beginning at the supplied start value, and a factory derived from the builder keeps that counter, incrementing once per thread it creates. So consecutive threads receive the prefix followed by 10 then 11.

  4. Question 4

    What does this print? ```java import java.util.ArrayList; import java.util.concurrent.ConcurrentLinkedQueue; public class Main { public static void main(String[] args) throws InterruptedException { var q = new ConcurrentLinkedQueue<Integer>(); var threads = new ArrayList<Thread>(); for (int i = 0; i < 50; i++) { int n = i; threads.add(Thread.ofVirtual().start(() -> q.add(n))); } for (Thread t : threads) { t.join(); } int sum = q.stream().mapToInt(Integer::intValue).sum(); System.out.print(q.size() + " " + sum); } } ```

    1. A. 0 0

      Assumes the reads happen before the threads run, but the join loop guarantees every task finished first.

    2. B. 50 1225Correct answer

      Fifty virtual threads each add their captured index (0..49) to a thread-safe ConcurrentLinkedQueue, and all are joined before the read, so size() is 50 and the sum 0+1+...+49 is 1225.

    3. C. 50 followed by a sum that varies between runs

      The sum cannot vary: addition is commutative and each element is added exactly once, so interleaving does not change the total.

    4. D. Throws ConcurrentModificationException

      ConcurrentLinkedQueue is built for concurrent adds, and the stream iterates it only after every writer has finished, so nothing is thrown.

    Explanation

    Joining every thread before reading the queue guarantees all fifty adds have happened, so the size is fifty and the elements are exactly 0 through 49. Because the aggregation is an order-independent sum over a thread-safe queue, the result is fully deterministic regardless of interleaving, yielding 1225.

  5. Question 5

    The executor is closed explicitly rather than with try-with-resources, and then reused. What does this print? ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; public class Main { public static void main(String[] args) { ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor(); ex.submit(() -> System.out.print("task ")); ex.close(); try { ex.submit(() -> System.out.print("late ")); System.out.print("accepted "); } catch (RejectedExecutionException e) { System.out.print("rejected "); } System.out.print(ex.isShutdown() + ":" + ex.isTerminated()); } } ```

    1. A. task rejected false:false

      Accepts that submission is refused but assumes the executor is not formally shut down; rejection is precisely the consequence of isShutdown() being true, so both flags are true.

    2. B. task rejected true:trueCorrect answer

      Correct: close() performs an orderly shutdown then awaits termination, so the first task runs, the second submit is rejected, and both isShutdown() and isTerminated() are already true.

    3. C. task accepted false:false

      Assumes close() is a no-op on a thread-per-task executor because there is no pool to tear down; the executor still has state and stops accepting work, so the resubmit is rejected.

    4. D. task rejected true:false

      Assumes isTerminated() flips only after an explicit awaitTermination call; close() already performs that wait, so termination has been observed by the time it returns.

    Explanation

    Trace: `close()` on an `ExecutorService` is defined as an orderly `shutdown()` followed by an unbounded wait for termination. So (1) the first task is guaranteed to run and print `task ` before `close()` returns — no `join()` and no `Future.get()` needed; (2) the executor is now shut down, so the second `submit()` is rejected with `RejectedExecutionException` and `rejected ` prints; (3) because `close()` already waited, both `isShutdown()` and `isTerminated()` are `true`. Output: `task rejected true:true`. Why the others are wrong: `task rejected true:false` encodes the belief that `isTerminated()` only flips after you explicitly call `awaitTermination(...)`. `close()` does that wait for you, so termination has already been observed by the time it returns. `task accepted false:false` encodes the belief that `close()` is a no-op on a thread-per-task executor because there is no pool to tear down. There is still executor state: it stops accepting work. `task rejected false:false` splits the difference — it accepts that submission is refused but assumes the executor is not formally "shut down". Rejection is precisely the consequence of `isShutdown()` being `true`. Exam tip: `ExecutorService` is `AutoCloseable` (since Java 19) and `close()` = `shutdown()` + await termination. A closed executor is dead, not paused — re-submitting throws `RejectedExecutionException`, it does not silently drop the task. The reverse trap is a stem that submits work, never joins, and expects nothing to print: inside try-with-resources, the implicit `close()` at the end of the block IS the join.

  6. Question 6

    A virtual thread is created unstarted, given the maximum priority, then started and joined. It reports on itself from the inside. What is printed? ```java import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) throws InterruptedException { AtomicReference<String> seen = new AtomicReference<>(); Thread t = Thread.ofVirtual().unstarted(() -> { Thread me = Thread.currentThread(); seen.set(me.isVirtual() + ":" + me.isDaemon() + ":[" + me.getName() + "]:" + me.getPriority()); }); t.setPriority(Thread.MAX_PRIORITY); t.start(); t.join(); System.out.println(seen.get() + " " + t.getState()); } } ```

    1. A. true:true:[]:10 TERMINATED

      Assumes setPriority takes effect; priority is silently ignored on a virtual thread and getPriority always returns the normal priority, 5.

    2. B. true:true:[]:5 TERMINATEDCorrect answer

      A virtual thread is virtual and always a daemon, has an empty name when none is set, and always reports the normal priority 5; join guarantees it is terminated.

    3. C. true:false:[]:5 TERMINATED

      Assumes a virtual thread can be non-daemon; virtual threads are always daemon threads, so isDaemon is true.

    4. D. true:true:[VirtualThread-0]:5 TERMINATED

      Assumes an unnamed virtual thread gets a generated pool-style name; its name is the empty string when none is set.

    Explanation

    Every virtual thread is virtual and is always a daemon, and one created without an explicit name has the empty string as its name. Its priority is fixed at the normal priority because the JDK scheduler ignores thread priorities for virtual threads, so setPriority has no effect. Joining guarantees the thread has finished, so its state is terminated.

  7. Question 7

    What does this print? ```java import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { AtomicInteger c = new AtomicInteger(); try (var ex = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 5; i++) { ex.submit(() -> c.incrementAndGet()); } } System.out.print(c.get()); } } ```

    1. A. 0

      Assumes close() abandons in-flight tasks; instead it waits for every submitted task to finish.

    2. B. 5Correct answer

      The try-with-resources close() blocks until all five submitted increments finish before c.get() runs, so the count is exactly 5.

    3. C. A number that varies between runs from 0 to 5

      The count is deterministic because close() joins every task before returning, so the final read cannot race the increments.

    4. D. Throws RejectedExecutionException

      RejectedExecutionException happens when submitting after shutdown; here every submit runs before the block ends.

    Explanation

    An ExecutorService used through try-with-resources runs close() when the block ends, and close() is equivalent to shutdown plus awaitTermination: it blocks until every submitted task has finished. Code after the block may therefore assume all tasks completed, so the atomic read is deterministic. All five increments run, giving a total of five.

  8. Question 8

    What does this print? ```java public class Main { public static void main(String[] args) { Thread p = Thread.ofPlatform().unstarted(() -> {}); Thread v = Thread.ofVirtual().unstarted(() -> {}); System.out.print(p.isVirtual() + " " + v.isVirtual()); } } ```

    1. A. true true

      Assumes both threads are virtual, but the platform builder explicitly constructs a platform thread, so its isVirtual() is false.

    2. B. true false

      Inverts the two builders, wrongly treating the platform builder as the one that produced the virtual thread.

    3. C. false trueCorrect answer

      The platform builder's unstarted thread is a platform thread (isVirtual() false) and the virtual builder's is a virtual thread (isVirtual() true); the kind is fixed at construction, not at run state, so the output is false true.

    4. D. Compilation fails: isVirtual is not defined for platform threads

      isVirtual() is a final method on Thread available for every thread, platform or virtual, so the code always compiles.

    Explanation

    A thread's kind is fixed at construction and never changes: the platform-thread builder produces a thread whose isVirtual() is false, while the virtual-thread builder produces one whose isVirtual() is true. This holds whether or not either thread was ever started, because isVirtual() reflects the kind chosen at creation rather than run state. So the two queries report false then true.

Practise all 17 Virtual Threads & Structured Concurrency questions

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

Open OCP Java SE 25

Other topics in this pack