Concurrency practice questions

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

Concurrency practice questions from OCP Java SE 25 (1Z0-831). This pack has 16 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

    What does this print? ```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(() -> 6 * 7); System.out.println(f.get()); es.shutdown(); } } ```

    1. A. The code does not compile: a lambda that returns a value cannot be passed to submit()

      submit is overloaded for Callable<T> as well as Runnable; a value-returning lambda binds to the Callable overload, so it compiles.

    2. B. 42Correct answer

      The lambda returns an int, so it is inferred as a Callable<Integer> and submit returns a Future<Integer>; f.get() blocks until the task finishes and returns 42. (Javadoc 25 — ExecutorService.submit(Callable).)

    3. C. 0

      0 would require a Runnable task (whose Future.get() yields null) autounboxing to 0; here the task is a Callable returning 42.

    4. D. null

      null is what a Runnable task's Future.get() returns; this task returns an Integer, not void.

    Explanation

    A lambda that returns a value binds to the Callable overload of submit, producing a Future that carries the computed result; get() blocks until the task completes and hands back that value. Had the lambda returned nothing, it would bind to the Runnable overload whose Future.get() always yields null. The lambda body decides the overload, and therefore what the Future carries.

  2. Question 2

    A bounded blocking queue is filled past its capacity. What does this print? ```java import java.util.concurrent.*; public class Main { public static void main(String[] args) throws Exception { BlockingQueue<Integer> q = new ArrayBlockingQueue<>(2); boolean a = q.offer(1); boolean b = q.offer(2); boolean c = q.offer(3); Integer head = q.poll(); q.put(4); System.out.println(a + " " + b + " " + c + " " + head + " " + q); } } ```

    1. A. true true false 1 [2, 4]Correct answer

      Capacity is fixed at 2, so offer(1) and offer(2) return true while offer(3) finds it full and returns false (silently dropping 3) rather than blocking or throwing; poll() removes the FIFO head 1, put(4) appends into the free slot, and the queue prints head-to-tail as [2, 4] — true true false 1 [2, 4].

    2. B. true true true 1 [2, 3, 4]

      Treats an ArrayBlockingQueue like an ArrayList that grows on demand; the capacity passed to the constructor is a hard bound, not an initial size hint, so offer(3) cannot succeed and 3 is not retained.

    3. C. The program throws IllegalStateException: Queue full

      This is the behaviour of add(3), not offer(3); add is the Collection-inherited method that throws IllegalStateException on a full queue, whereas offer reports failure by returning false.

    4. D. true true false 3 [2, 4]

      Gets the insertion behaviour right but polls from the wrong end, treating the queue as a stack; poll() retrieves and removes the head — the oldest element, 1 — not the most recent.

    Explanation

    Trace: the queue's capacity is fixed at 2 by the constructor. `offer(1)` and `offer(2)` fill it and return `true`. `offer(3)` finds it full and — this is the contract of `offer` — reports failure by returning `false` rather than blocking or throwing, so 3 is silently dropped. `poll()` removes the *head*, and the queue is FIFO, so it returns `1` and leaves one free slot. `put(4)` would block if the queue were full, but it is not, so 4 is appended immediately. The queue's toString prints head-to-tail: `[2, 4]`. Output: `true true false 1 [2, 4]`. Why the others are wrong: `true true true 1 [2, 3, 4]` treats an ArrayBlockingQueue like an ArrayList that grows on demand; the capacity passed to the constructor is a hard bound, not an initial size hint. `true true false 3 [2, 4]` gets the insertion behaviour right but polls from the wrong end — it treats the queue as a stack. `poll()` retrieves and removes the head, i.e. the oldest element. `The program throws IllegalStateException: Queue full` is the behaviour of `add(3)`, not `offer(3)`. Confusing the two is the whole point of the question: `add` is the Collection-inherited method that must throw on failure. Exam tip: BlockingQueue offers three insertion styles for the full-queue case and three removal styles for the empty-queue case. Insert: `add` throws IllegalStateException, `offer` returns false, `put` blocks (and `offer(e, timeout, unit)` blocks then returns false). Remove: `remove` throws NoSuchElementException, `poll` returns null, `take` blocks. The reverse trap is `poll()` on an empty queue — it returns `null`, it does not throw, so it will NPE on you later when you unbox it into an `int`.

  3. Question 3

    A Callable submitted to an executor throws while running, and its Future.get() is called inside a try/catch. What does this print? ```java import java.util.concurrent.*; public class Main { public static void main(String[] args) throws InterruptedException { ExecutorService es = Executors.newSingleThreadExecutor(); Future<Integer> f = es.submit(() -> 10 / 0); try { f.get(); System.out.println("no error"); } catch (ExecutionException e) { System.out.println("caught " + e.getCause().getClass().getSimpleName()); } es.shutdown(); } } ```

    1. A. caught ExecutionException

      This would print if the code used e.getClass() instead of e.getCause().getClass(); the code deliberately unwraps the cause.

    2. B. no error

      The task really does divide by zero and throw, so get() cannot reach the no-error line; control enters the catch instead.

    3. C. The task's ArithmeticException propagates out of submit() and terminates main

      A task's failure never propagates from submit(); submit only enqueues the task, and the throwable is delivered at get() wrapped in an ExecutionException.

    4. D. caught ArithmeticExceptionCorrect answer

      The worker's ArithmeticException is captured in the Future and rethrown by get() wrapped in an ExecutionException; e.getCause() is the original ArithmeticException, so its simple name prints. (Javadoc 25 — Future.get / ExecutionException.)

    Explanation

    A task that throws while running on a worker thread does not fail at submit time; the throwable is stored in the Future and surfaces only when get() is called, which rethrows it wrapped in an ExecutionException. Unwrapping that with getCause() recovers the original exception, so the printed simple name is the task's own exception type. Any claim that the exception escapes at submit() is wrong.

  4. Question 4

    A Runnable is submitted to a single-threaded executor and the returned Future is waited on. What does this print? ```java import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) throws Exception { ExecutorService es = Executors.newSingleThreadExecutor(); AtomicInteger tally = new AtomicInteger(); Runnable job = () -> tally.addAndGet(4); Future<?> f = es.submit(job); Object result = f.get(); es.shutdown(); System.out.println(result + " " + tally.get() + " " + f.isDone()); } } ```

    1. A. 0 4 true

      Assumes get() fills in a default zero for the missing result; the Future's result type is unbounded and its value is literally the null reference, not 0.

    2. B. null 4 trueCorrect answer

      Correct: job is typed Runnable, so submit(Runnable) returns a Future whose get() yields null; the side effect still ran (tally is 4), and isDone() reports task completion, so it is true.

    3. C. null 4 false

      Assumes a Future is not done until the executor terminates; isDone() is a per-task flag that flips true as soon as the task completes, cancels, or throws.

    4. D. 4 4 true

      Assumes the Future captures whatever the lambda body evaluated to; that only happens for submit(Callable<T>), and assigning the lambda to Runnable first erases the value, so get() returns null.

    Explanation

    Trace: `job` is typed as a `Runnable`, so `es.submit(job)` selects the `submit(Runnable)` overload, which returns a `Future<?>` that completes with `null` — a Runnable has no result to carry, and the `tally.addAndGet(4)` value is simply discarded by the void-compatible lambda body. `f.get()` therefore blocks until the task finishes and then hands back `null`. The side effect still happened, so `tally.get()` is `4`, and because `get()` returned normally the task is finished, so `f.isDone()` is `true` — `isDone()` reports task completion, not executor shutdown. Output: `null 4 true`. Why the others are wrong: `4 4 true` assumes the Future captures whatever the lambda body evaluated to; that only happens for `submit(Callable<T>)`, where the lambda's value is the result. Assigning the lambda to `Runnable` first is what erases the value. `null 4 false` encodes the belief that a Future is not "done" until the executor terminates. `isDone()` is a per-task flag: it flips to true as soon as the task completes, is cancelled, or throws. `0 4 true` assumes `get()` fills in a default zero value for the missing result; the Future's result type is unbounded (`Future<?>`) and its value is literally the null reference. Exam tip: the overload is chosen by the *declared type* of what you pass, not by whether the lambda body happens to produce a value. `submit(Runnable)` → `get()` returns `null`; `submit(Runnable, T result)` → `get()` returns that pre-supplied `result`; `submit(Callable<T>)` → `get()` returns the callable's value. The reverse trap is passing a value-producing lambda directly to `submit(...)` with no target type in sight — then it is a `Callable` and `get()` gives you `4`.

  5. Question 5

    What does this print? ```java import java.util.concurrent.*; import java.util.*; public class Main { public static void main(String[] args) { CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(List.of(1, 2, 3)); int sum = 0; for (Integer i : list) { list.add(99); sum += i; } System.out.println(sum + " " + list.size()); } } ```

    1. A. Throws ConcurrentModificationException

      That is what a plain ArrayList would do; COW iterators are snapshot-based and never throw ConcurrentModificationException.

    2. B. 6 6Correct answer

      A CopyOnWriteArrayList iterator walks a snapshot of the three original elements, so the loop runs three times (sum 1+2+3=6) while each add(99) grows the live list to six elements. (Javadoc 25 — CopyOnWriteArrayList.)

    3. C. The loop never terminates because each iteration appends another element

      The loop cannot see the appended elements (they land on a newer copy), so it terminates after the three snapshot elements.

    4. D. 6 3

      6 3 assumes the adds were discarded; the adds genuinely succeed and the list ends at size 6, even though the iterator ignored them.

    Explanation

    A CopyOnWriteArrayList iterator is created over a snapshot of the backing array taken at construction, so mutations during iteration go to a fresh copy the iterator never sees. The loop therefore visits exactly the original elements and never throws, while the additions still succeed and enlarge the live list. This trades write cost for iteration safety — ideal for read-mostly data, wrong when writes dominate.

  6. Question 6

    What does this print? ```java import java.util.concurrent.*; import java.util.*; public class Main { public static void main(String[] args) throws Exception { ExecutorService es = Executors.newFixedThreadPool(3); List<Callable<Integer>> tasks = List.of(() -> 1, () -> 2, () -> 3); int total = 0; for (Future<Integer> f : es.invokeAll(tasks)) { total += f.get(); } System.out.println(total); es.shutdown(); } } ```

    1. A. 6Correct answer

      invokeAll blocks until every task completes and returns the Futures in task order, so each get() returns immediately and the sum is 1+2+3=6. (Javadoc 25 — ExecutorService.invokeAll.)

    2. B. 0

      0 assumes the Futures are not finished yet; invokeAll blocks until all tasks complete, so no get() ever returns a default.

    3. C. A value that varies between runs because the three tasks run concurrently

      Although the tasks run concurrently, the SUM of their results is fixed regardless of ordering; concurrency does not make 1+2+3 vary.

    4. D. The program deadlocks: each get() blocks forever waiting for its task

      get() cannot block forever here: invokeAll already waited for completion, so every returned Future reports done.

    Explanation

    invokeAll runs the whole batch and returns only after all tasks have finished, handing back a list of completed Futures in the SAME order as the submitted tasks. Every get() therefore returns its result immediately, and because addition is order-independent the total is deterministic despite concurrent execution. Contrast with invokeAny, which returns a single result as soon as any task succeeds, and submit, which returns immediately with a still-pending Future.

  7. Question 7

    All of this runs on the main thread. What is printed? ```java import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { AtomicInteger counter = new AtomicInteger(5); int first = counter.getAndIncrement(); int second = counter.incrementAndGet(); boolean swapped = counter.compareAndSet(6, 100); ConcurrentHashMap<String, Integer> tally = new ConcurrentHashMap<>(); tally.put("k", 1); tally.compute("k", (key, value) -> null); System.out.println(first + " " + second + " " + swapped + " " + counter.get() + " " + tally.size()); } } ```

    1. A. 5 7 true 100 0

      Assumes the compare-and-set succeeds; the current value is already 7, so it fails and the counter stays 7.

    2. B. 6 7 false 7 1

      Inverts getAndIncrement, which returns the value before incrementing, and assumes the map entry survives.

    3. C. 5 7 false 7 1

      Assumes the map entry survives a remapping that returns null; such a remapping removes the mapping, leaving size zero.

    4. D. 5 7 false 7 0Correct answer

      getAndIncrement returns 5 leaving 6, incrementAndGet returns 7, compareAndSet against an expected 6 fails since the value is now 7, and a compute returning null removes the entry for size zero.

    Explanation

    getAndIncrement returns the pre-increment value while incrementAndGet returns the post-increment value, and compareAndSet compares against the current value, so it fails once that value has moved on. A ConcurrentHashMap forbids null values, so a remapping function that returns null removes the mapping rather than storing null.

  8. Question 8

    What does this print? ```java import java.util.concurrent.*; public class Main { public static void main(String[] args) { ConcurrentHashMap<String, Integer> m = new ConcurrentHashMap<>(); Integer first = m.putIfAbsent("k", 1); Integer second = m.putIfAbsent("k", 2); System.out.println(first + " " + second + " " + m.get("k")); } } ```

    1. A. null null 1

      Assumes putIfAbsent always returns null; the second call must report the existing value 1.

    2. B. null 1 2

      Assumes the second call overwrote the mapping to 2; putIfAbsent never replaces an existing value.

    3. C. null 1 1Correct answer

      putIfAbsent returns the previous value (null on first insert) and writes only when the key was absent, so the first call returns null and stores 1, the second returns the existing 1 and changes nothing, leaving the mapping at 1. (Javadoc 25 — ConcurrentHashMap.putIfAbsent.)

    4. D. 1 1 1

      Assumes the first call returns the value it just stored; on insertion putIfAbsent returns null, not the new value.

    Explanation

    putIfAbsent returns the value previously associated with the key — null when there was none — and stores the new value only when the slot was empty. The first call therefore reports null while installing its value, and a second call on the now-occupied key reports the existing value and leaves the map untouched. It is the read-and-maybe-write primitive behind lazy initialization, so do not confuse its return value with the value now in the map.

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