Concurrency practice questions

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

Concurrency practice questions from OCP Java SE 21 (1Z0-830). This pack has 18 questions tagged 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 Concurrency

  1. Question 1

    Which two statements correctly distinguish Callable from Runnable? (Choose two.)

    1. A. Callable.call() returns a value, while Runnable.run() returns voidCorrect answer

      Callable<V> declares V call(), producing a result, whereas Runnable declares void run() and returns nothing, the defining signature difference.

    2. B. Callable.call() can throw checked exceptions, while Runnable.run() declares noneCorrect answer

      call() is declared throws Exception, so it may propagate checked exceptions, while run() has an empty throws clause and must handle or wrap checked exceptions internally.

    3. C. Only Runnable can be submitted to an ExecutorService

      ExecutorService.submit is overloaded for both; a submitted Runnable yields a Future whose get() returns null (or a supplied result value).

    4. D. Callable tasks always run on a newly created platform thread

      Where a task runs is the executor's policy, pooled platform threads, per-task virtual threads, or even the calling thread, and the task interface says nothing about it.

    Explanation

    The distinction is entirely in the two method signatures: V call() throws Exception versus void run(). Callable can both return a result and propagate a checked exception; Runnable can do neither and must catch or wrap any checked exception inside run(). Neither interface constrains which executor or thread runs the task, and both can be submitted to an ExecutorService, so a value-returning or checked-throwing lambda passed to submit is inferred as a Callable while a void lambda is a Runnable.

  2. Question 2

    What does this print? ```java import java.util.concurrent.atomic.*; public class Main { public static void main(String[] args) throws InterruptedException { AtomicInteger count = new AtomicInteger(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { count.incrementAndGet(); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count.get()); } } ```

    1. A. 1000

      Would require one thread's updates to vanish entirely, but atomicity forbids lost updates.

    2. B. A value between 1000 and 2000 that varies from run to run

      This is what a plain int with count++ would produce through a data race; AtomicInteger exists precisely to make the result deterministic.

    3. C. 0

      Would mean neither thread ran, but start() followed by join() guarantees both completed.

    4. D. 2000Correct answer

      Each thread performs 1000 atomic increments and incrementAndGet is an atomic read-modify-write, so no update is lost; both join() calls complete before the read, giving 2 x 1000 = 2000.

    Explanation

    incrementAndGet is an atomic read-modify-write, so concurrent increments from both threads are never lost, unlike a plain int with count++, which can drop updates and yield a varying total. Two threads each adding 1000 therefore produce exactly 2000. The join() calls are load-bearing: they ensure both threads finish before the counter is read, so no in-progress value is printed.

  3. Question 3

    A ConcurrentHashMap is queried for a missing key and then offered a null value. What is the output? ```java import java.util.*; import java.util.concurrent.*; public class Main { public static void main(String[] args) { Map<String, String> m = new ConcurrentHashMap<>(); m.put("k", "v"); System.out.print(m.get("missing") + " "); try { m.putIfAbsent("n", null); System.out.print("stored "); } catch (NullPointerException e) { System.out.print("NPE "); } System.out.println(m.size()); } } ```

    1. A. NPE NPE 1

      Assumes the null-hostility extends to lookups, so get of an absent key would also throw. get returns null for a missing key exactly as any Map does; only storing null is banned, so the first token is null, not NPE.

    2. B. null NPE 2

      Gets the exception right but assumes the entry was inserted before the null check ran, leaving size 2. The null check happens first, so the map is untouched and size stays 1.

    3. C. null stored 2

      Assumes ConcurrentHashMap behaves like HashMap and stores a null value. It forbids null keys and values by design, so putIfAbsent throws rather than storing, and size stays 1.

    4. D. null NPE 1Correct answer

      get of a missing key returns null, then putIfAbsent with a null value throws NullPointerException before any entry is created, so the map still holds only k=v and size() is 1.

    Explanation

    Trace: `get("missing")` on a `ConcurrentHashMap` is an ordinary lookup that finds nothing and returns `null` — printed as `null`. Then `putIfAbsent("n", null)` is rejected: `ConcurrentHashMap` forbids `null` keys *and* `null` values, and throws `NullPointerException` before any entry is created. The catch block prints `NPE `. Nothing was added, so the map still holds only `k=v` and `size()` is `1`. Why the others are wrong: `null stored 2` encodes the belief that `ConcurrentHashMap` behaves like `HashMap`, which happily stores a `null` value. It does not — that is a deliberate design difference. `NPE NPE 1` assumes the null-hostility extends to lookups, so that `get` of an absent key would also blow up. `get` returns `null` for a missing key exactly as any `Map` does; only *storing* `null` is banned. `null NPE 2` gets the exception right but assumes the entry was inserted before the check ran, leaving a partially-written map. The null check happens first, so the map is untouched. Exam tip: `ConcurrentHashMap`, `ConcurrentSkipListMap` and `Hashtable` all reject null keys and values; `HashMap` and `TreeMap` (with a null-tolerant comparator) allow null values. The reason is ambiguity — in a concurrent map, a `get` returning `null` must unambiguously mean "absent", not "present but mapped to null". That is also why `getOrDefault` exists.

  4. Question 4

    Two compare-and-set attempts are made against the same atomic, and a copy-on-write list is grown while it is being iterated — all on the main thread. Determine the output. ```java import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; public class Main { public static void main(String[] args) { AtomicInteger a = new AtomicInteger(10); boolean first = a.compareAndSet(10, 20); boolean second = a.compareAndSet(10, 30); List<String> list = new CopyOnWriteArrayList<>(List.of("x", "y")); StringBuilder sb = new StringBuilder(); for (String s : list) { sb.append(s); list.add("z"); } System.out.println(first + " " + second + " " + a.get() + " " + sb + " " + list.size()); } } ```

    1. A. true true 30 xy 4

      Assumes both compare-and-sets succeed as unconditional writes; the second expects the original value but it is already updated, so it fails and leaves the value at 20.

    2. B. Throws ConcurrentModificationException

      That is what a plain list would do; a copy-on-write list iterates over a snapshot, so structural changes during iteration never trigger a concurrent-modification exception.

    3. C. true false 20 xyz 3

      Assumes the iterator sees elements added during the loop; it walks the snapshot taken at creation, so only the original elements are visited even as two more are added.

    4. D. true false 20 xy 4Correct answer

      The first compare-and-set succeeds (value 20) and the second fails (value stays 20); the snapshot iterator visits only "xy" while two appends grow the list to size 4.

    Explanation

    Compare-and-set writes only when the current value equals the expected value and reports whether it did, so a second attempt with a stale expected value fails and leaves the value unchanged. A copy-on-write list's iterator traverses an immutable snapshot captured at creation, so it never throws on concurrent structural modification and never observes elements added during the traversal, though those additions still grow the list.

  5. Question 5

    What is the output of the following program? ```java import java.util.concurrent.*; public class Main { public static void main(String[] args) throws Exception { ExecutorService es = Executors.newSingleThreadExecutor(); Future<Integer> f = es.submit(() -> {}, 42); es.shutdown(); System.out.println(f.get()); } } ```

    1. A. null

      Confuses `submit(Runnable)` — the single-argument form, whose `Future.get()` always yields null because a Runnable produces no value — with the two-argument `submit(Runnable, T)` used here. The second argument, 42, is the caller-prescribed result that the framework records and returns; it is not ignored.

    2. B. 42Correct answer

      `ExecutorService.submit(Runnable task, T result)` submits the Runnable and records the caller-supplied value as what `Future.get()` returns upon successful completion (Javadoc: 'The Future's get method will return the given result upon successful completion'). The body `() -> {}` completes normally, so `get()` returns the boxed Integer 42, which `println` renders as `42`.

    3. C. Compilation fails

      The code compiles without error. `ExecutorService` declares the generic overload `<T> Future<T> submit(Runnable task, T result)`, so `submit(() -> {}, 42)` is valid: the lambda `() -> {}` is void-compatible and satisfies `Runnable`, and the literal `42` is autoboxed to `Integer` to bind `T`. The checked exceptions that `get()` may throw are covered by `throws Exception` on `main`.

    4. D. Throws `ExecutionException` at runtime

      `Future.get()` wraps task exceptions in `ExecutionException` only when the task itself throws. The Runnable `() -> {}` has an empty body and completes normally, so there is nothing to wrap and `get()` returns the prescribed result cleanly. `shutdown()` before `get()` does not cancel an already-queued task; it only prevents new submissions.

    Explanation

    The three `ExecutorService.submit` overloads differ in how `Future.get()` resolves: the single-argument `submit(Runnable)` always returns null, `submit(Callable<T>)` returns the callable's return value, and `submit(Runnable, T)` returns the caller-supplied result — here 42 — when the Runnable completes without throwing. The two-argument form is a genuine API method that compiles without error; `shutdown()` permits already-queued tasks to run to completion before the pool stops, so `get()` returns normally and no `ExecutionException` is raised.

  6. Question 6

    A named virtual thread is built, started, and joined before the main thread prints. What is the output? ```java public class Main { public static void main(String[] args) throws InterruptedException { StringBuilder sb = new StringBuilder(); Thread t = Thread.ofVirtual().name("worker").unstarted(() -> sb.append("ran ")); t.start(); t.join(); System.out.println(sb + t.getName() + " " + t.isVirtual() + " " + t.isDaemon()); } } ```

    1. A. ran worker true false

      Assumes a virtual thread is a normal non-daemon user thread that keeps the JVM alive. Virtual threads are ALWAYS daemons, so isDaemon() is true — which is why you must join them or close their executor.

    2. B. ran worker false true

      Assumes name(...) is decorative and the JVM still auto-numbers the thread. Thread-N names come from the platform-thread constructor; here name("worker") was set explicitly, so getName() is "worker".

    3. C. ran worker true trueCorrect answer

      unstarted returns a not-yet-started virtual Thread; start()+join() make the append happen-before main's read (ran); getName() is the set name "worker"; isVirtual() is true; and every virtual thread is a daemon, so isDaemon() is true.

    4. D. ran Thread-0 true true

      Assumes isVirtual() reports on the CARRIER platform thread, so it would read false when queried from main. isVirtual() is a property of the Thread object itself, not of whoever asks, so it is true.

    Explanation

    Trace: `Thread.ofVirtual()` returns a builder. `.name("worker")` sets the thread's name, and `unstarted(Runnable)` hands back a not-yet-started virtual `Thread`. `start()` schedules it on the fork-join carrier pool; `join()` blocks main until it finishes, so the append to `sb` happens-before main reads it — the output is fully deterministic despite the shared StringBuilder. `sb` therefore holds `ran `, `getName()` is `worker`, `isVirtual()` is `true`, and every virtual thread is a daemon thread, so `isDaemon()` is `true` as well. Why the others are wrong: `ran worker true false` encodes the belief that a virtual thread is a normal user (non-daemon) thread that keeps the JVM alive. Virtual threads are always daemons — that is exactly why you must join them or close their executor. `ran Thread-0 true true` assumes the builder's `name(...)` call is decorative and the JVM still auto-numbers the thread. `Thread-N` names come from the platform-thread constructor; an unnamed virtual thread's name is the empty string, and here `name("worker")` was set explicitly. `ran worker false true` encodes the idea that `isVirtual()` reports on the *carrier* platform thread, so it would read `false` when the object is queried from main. `isVirtual()` is a property of the Thread object itself, not of whoever asks. Exam tip: memorise the virtual-thread defaults — daemon always, priority always `NORM_PRIORITY` (5), no thread group you can rely on, and `setDaemon(false)` on one throws. The reverse trap is a stem that starts a virtual thread and never joins it: `main` returns, and because the virtual thread is a daemon the JVM exits without its output ever appearing.

  7. Question 7

    A task submitted to a single-threaded executor blows up, and the executor is then shut down before another task is offered to it. What appears on the console? ```java import java.util.concurrent.*; public class Main { public static void main(String[] args) throws Exception { ExecutorService es = Executors.newSingleThreadExecutor(); Future<Integer> f = es.submit(() -> Integer.parseInt("21x")); String a; try { a = String.valueOf(f.get()); } catch (ExecutionException e) { a = e.getCause().getClass().getSimpleName(); } es.shutdown(); String b; try { es.submit(() -> 1); b = "accepted"; } catch (RejectedExecutionException e) { b = "rejected"; } System.out.println(a + " " + b); } } ```

    1. A. NumberFormatException rejectedCorrect answer

      The task's exception is captured in the Future and surfaces from the result retrieval wrapped in an ExecutionException, whose cause is the original parse failure; after shutdown, a new submission is refused as rejected.

    2. B. ExecutionException rejected

      Reads the wrapper's own type instead of its cause; unwrapping the ExecutionException gives the underlying parse exception.

    3. C. NumberFormatException accepted

      Assumes shutdown only takes effect once the pool is idle; shutdown immediately stops accepting new tasks, so the second submission is rejected.

    4. D. Throws NumberFormatException

      Assumes the task exception propagates directly; submission returns immediately and stores the exception in the Future, so it only appears, wrapped, when the result is retrieved.

    Explanation

    A task's exception does not escape at submission time; it is stored in the returned Future and re-thrown, wrapped in an ExecutionException, only when the result is retrieved, so the original type is reached through the cause. Initiating shutdown lets already-submitted tasks finish but immediately stops the executor from accepting new work, which it refuses with an unchecked rejection exception.

  8. Question 8

    What is the output of the following program? ```java import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { AtomicInteger x = new AtomicInteger(4); int a = x.updateAndGet(v -> v * v); int b = x.getAndUpdate(v -> v + 4); System.out.println(a + " " + b + " " + x.get()); } } ```

    1. A. 16 16 20Correct answer

      updateAndGet(v -> v * v) applies v*v to 4, stores 16, and returns the NEW value 16, so a = 16. getAndUpdate(v -> v + 4) captures and returns the CURRENT value 16 (so b = 16) and then stores 20. x.get() is 20.

    2. B. 4 16 20

      Treats updateAndGet as returning the value before the operation fires — the behaviour of getAndUpdate. The name ending in ...AndGet signals that the function is applied first and the new value is what the call returns.

    3. C. 16 20 20

      Treats getAndUpdate as returning the new value (20) after the function is applied — the behaviour of updateAndGet. The name starting with get... signals that the pre-operation value is captured and returned before the function runs.

    4. D. 4 20 20

      Swaps the semantics of both methods at once: updateAndGet taken to return old (4) and getAndUpdate taken to return new (20). The two methods are mirror images of each other, so reversing both simultaneously cannot produce a correct result.

    Explanation

    AtomicInteger method names encode which value the call returns relative to when the operation fires. A method whose name begins with `get` (get-and-X pattern) captures the current value first and returns it, then applies the operation — the caller receives the OLD value. A method whose name ends in `get` (X-and-get pattern) applies the operation first and returns the result — the caller receives the NEW value. Misreading `updateAndGet` as returning the pre-update value, misreading `getAndUpdate` as returning the post-update value, or swapping both simultaneously, each produce a distinct wrong answer. Tracing the correct execution: `updateAndGet(v -> v * v)` stores 4 × 4 = 16 in `x` and returns 16; `getAndUpdate(v -> v + 4)` returns 16 and then stores 20.

Practise all 18 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