Concurrency practice questions

From OCP Java SE 17 (1Z0-829) · 18 questions on this topic

Concurrency practice questions from OCP Java SE 17 (1Z0-829). 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

    What does Future.get() do if the task has not yet completed?

    1. A. It blocks the calling thread until the task finishes (or the timeout overload elapses)Correct answer

      get() parks the calling thread until the task completes and then returns its result, and the timed overload get(timeout, unit) gives up after the timeout with a TimeoutException.

    2. B. It cancels the task

      Cancellation is a separate, explicit call, cancel(mayInterruptIfRunning), not something get() performs.

    3. C. It throws IllegalStateException

      Calling get() before completion is the normal, supported pattern, not an illegal state.

    4. D. It returns null immediately

      get() never returns a placeholder; a null result only ever means the task itself produced null, such as with submit(Runnable).

    Explanation

    Future.get() is a blocking call: when the task is not finished it parks the calling thread until completion, then returns the result or throws ExecutionException if the task failed. The timed overload blocks similarly but abandons the wait with a TimeoutException once the deadline passes. This blocking behavior is distinct from isDone(), which returns immediately, and from cancellation, which is a separate explicit call.

  2. Question 2

    What does this print? ```java import java.util.concurrent.*; public class Main { public static void main(String[] args) throws Exception { ExecutorService es = Executors.newFixedThreadPool(2); Future<Integer> f = es.submit(() -> 6 * 7); System.out.println(f.get()); es.shutdown(); } } ```

    1. A. 42Correct answer

      The lambda () -> 6 * 7 is taken as a Callable<Integer> whose Future.get() blocks main until a pool thread computes 42 and returns it, so 42 prints (Javadoc 17: ExecutorService.submit / Future.get).

    2. B. The program hangs because the executor is never terminated

      The program cannot hang: get() returns as soon as the trivial task completes and shutdown() then releases the workers.

    3. C. Compilation fails: the call to submit is ambiguous

      There is no ambiguity: 6 * 7 is an expression, not a valid statement, so the Runnable overload is inapplicable and only submit(Callable) matches.

    4. D. 0

      0 would be a default value, but get() returns the task's actual computed result.

    Explanation

    The lambda body 6 * 7 is an expression that yields a value, so it can only be a Callable<Integer>, and submit returns a Future<Integer> immediately. Calling get() blocks the main thread until a pool thread computes the result, which is printed as 42. shutdown() afterwards lets the non-daemon workers exit so the JVM terminates, and because shutdown() still allows an already-submitted task to finish, reordering it before get() would print the same result.

  3. Question 3

    What does this print? ```java import java.util.concurrent.atomic.*; public class Main { public static void main(String[] args) { AtomicInteger a = new AtomicInteger(5); int r = a.getAndIncrement(); System.out.println(r + " " + a.get()); } } ```

    1. A. 6 5

      This would mean getAndIncrement returned the new value and left the counter unchanged, but it does the opposite on both counts.

    2. B. 5 6Correct answer

      getAndIncrement() is the atomic post-increment: it returns the old value 5 and then stores 6, so the captured value is 5 and a later read is 6 (Javadoc 17: AtomicInteger.getAndIncrement).

    3. C. 6 6

      This describes incrementAndGet(), the pre-increment twin that returns the new value.

    4. D. 5 5

      This would mean nothing was incremented, but the atomic update always happens.

    Explanation

    getAndIncrement() is the atomic post-increment: it returns the value held before the update and only then stores the incremented value. So the captured return is the original 5 while a later read of the counter sees 6. Map the names literally: getAndIncrement() behaves like i++ (old value back) whereas incrementAndGet() behaves like ++i (new value back).

  4. Question 4

    A task is handed to a pool and the caller then waits on the returned Future. Exactly what does this program print? ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(2); StringBuilder log = new StringBuilder(); Future<?> future = pool.submit(() -> { log.append("task"); }); Object value = future.get(); pool.shutdown(); System.out.println(log + " " + value + " " + future.isDone()); } } ```

    1. A. task null trueCorrect answer

      The block-bodied lambda is a Runnable, so submit returns a Future whose get() returns null after the task completes; the task mutated the shared StringBuilder to "task" and isDone() is true.

    2. B. task task true

      Assumes the Future captures what the lambda body evaluated to; only submit(Callable) does that, and a block-bodied lambda binds to Runnable, so get() returns null, not the builder.

    3. C. null null true

      Assumes the task never ran or the worker sees its own copy of log; the task runs and mutates the same StringBuilder, and get() establishes a happens-before edge making "task" visible.

    4. D. task null false

      Assumes isDone() flips only after the pool is shut down; isDone() is about the single task and is true as soon as that task completes.

    Explanation

    Trace: the lambda has a block body whose only statement is `log.append("task");`, and that statement's value is discarded, so the lambda is compatible with `Runnable` (void) — `submit(Runnable)` is chosen. That overload returns a `Future<?>` that carries no result: `get()` waits for the task to finish and then returns `null`. `get()` returning at all means the task has completed, so `isDone()` is `true`, and the `StringBuilder` has been appended to. Output: `task null true`. Why the others are wrong: `task task true` assumes the `Future` captures whatever the lambda body evaluated to. Only `submit(Callable<T>)` does that; had the lambda been written as an expression body `() -> log.append("task")`, `append` returns the `StringBuilder`, the lambda would bind to `Callable` and `get()` really would return the builder — the braces are what decide it. `task null false` assumes `isDone()` only flips once the executor is shut down or terminated. `isDone()` is about the one task, and it is true as soon as that task completes normally, is cancelled, or throws. `null null true` assumes the submitted task never ran, or that the pool's worker sees its own copy of `log`. The task runs on a pool thread but mutates the very same `StringBuilder` object the main thread holds, and `get()` establishes a happens-before edge so that mutation is visible. Exam tip: `submit(Runnable)` gives you a `Future<?>` whose `get()` always yields `null` — its only jobs are to block until completion and to report a thrown exception as `ExecutionException`. If you need a value, the task must be a `Callable`.

  5. Question 5

    Which statement correctly contrasts ExecutorService.shutdown() with shutdownNow()?

    1. A. shutdown() blocks until every submitted task has completed

      Neither method blocks; waiting for completion is the separate call awaitTermination(timeout, unit).

    2. B. shutdown() stops new submissions but lets already-submitted tasks run to completion; shutdownNow() additionally attempts to stop executing tasks (typically via interrupt) and returns the tasks that never startedCorrect answer

      shutdown() is graceful, rejecting new tasks but letting previously submitted and queued tasks run to completion, while shutdownNow() halts intake, attempts to interrupt actively executing tasks, skips the queue, and returns the List<Runnable> of tasks that never began (Javadoc 17: ExecutorService.shutdown / shutdownNow).

    3. C. The two methods differ only in whether the task queue is drained; both wait for running tasks to finish

      They differ in more than queue handling, since only shutdownNow() interrupts running tasks, and neither one waits for running tasks to finish.

    4. D. After shutdown(), further submit() calls are silently ignored

      Submissions after shutdown() are not ignored; they fail fast with RejectedExecutionException.

    Explanation

    shutdown() is the graceful stop: it refuses new submissions but allows already-submitted and queued tasks to run to completion. shutdownNow() is abrupt: it stops intake, makes a best-effort attempt to cancel running tasks (typically by interrupting their threads), skips the pending queue, and returns the tasks that never started. Neither method blocks, since awaitTermination is the separate call for that, and because interruption is only best-effort a task that ignores its interrupted status may keep running.

  6. Question 6

    What does this program print? ```java import java.util.concurrent.ConcurrentHashMap; public class Main { public static void main(String[] args) { ConcurrentHashMap<String, Integer> stock = new ConcurrentHashMap<>(); stock.put("bolt", 1); Integer first = stock.putIfAbsent("bolt", 9); Integer second = stock.putIfAbsent("nut", 4); System.out.println(first + " " + second + " " + stock.get("bolt") + " " + stock.size()); } } ```

    1. A. 1 4 1 2

      Assumes putIfAbsent returns the value it just installed for an absent key; it returns the previous mapping, which for the absent "nut" is null, not 4.

    2. B. 1 null 9 2

      Treats putIfAbsent as a plain put that returns the old value; it does not overwrite an existing mapping, so "bolt" stays 1, not 9.

    3. C. null null 1 2

      Assumes putIfAbsent always returns null; a non-null return is the signal the key was already taken, so the first call returns the existing 1.

    4. D. 1 null 1 2Correct answer

      "bolt" already maps to 1, so its write is skipped and 1 is returned while the value stays 1; "nut" is absent, so 4 is stored and null returned, leaving size 2.

    Explanation

    Trace: `putIfAbsent` writes the value only when the key has no mapping, and it returns the value that was *already* there — or `null` when there was none. `bolt` is already mapped to `1`, so the second write is skipped and `1` is returned; `bolt` keeps the value `1`, not `9`. `nut` is absent, so `4` is stored and `null` is returned. The map ends with two keys, so `1 null 1 2` is printed. Why the others are wrong: `1 4 1 2` assumes that for an absent key `putIfAbsent` hands back the value it just installed; it returns the *previous* mapping, which for an absent key is `null`. `1 null 9 2` treats `putIfAbsent` as a plain `put` that happens to return the old value — but the whole point of the method is that it does not overwrite an existing mapping, so `bolt` stays at `1`. `null null 1 2` assumes `putIfAbsent` always returns `null` (as if the return value only signalled "nothing to report"). A non-null return is precisely the signal that the key was taken and your value was discarded. Exam tip: `putIfAbsent` returns the current value, so `null` means "I stored yours" and non-null means "I did not". The reverse trap: `ConcurrentHashMap` allows neither null keys nor null values, which is exactly what makes that `null` return unambiguous — on a `HashMap`, a `null` return could also mean "mapped to null".

  7. Question 7

    This program mutates a concurrent list while a for-each loop is walking it, then performs one atomic compare-and-set. All of it runs on the main thread. What is printed? ```java import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { List<String> names = new CopyOnWriteArrayList<>(List.of("ada", "bob")); AtomicInteger visits = new AtomicInteger(); for (String n : names) { visits.getAndIncrement(); names.add(n.toUpperCase()); } boolean swapped = visits.compareAndSet(2, 10); System.out.println(names.size() + " " + visits.get() + " " + swapped); } } ```

    1. A. 4 10 trueCorrect answer

      Correct — the snapshot iterator walks the original two elements while the two appends grow the live list to size 4; the counter is left at 2, so compareAndSet(2, 10) matches, writes 10, and returns true.

    2. B. 4 2 true

      Assumes compareAndSet only tests the value; when the expected value matches it also writes the new value, so the counter becomes 10, not 2.

    3. C. Throws ConcurrentModificationException

      Expects the fail-fast behaviour of a plain ArrayList; a copy-on-write list iterates over a snapshot, so mutating it during iteration throws nothing.

    4. D. The loop never terminates, because every pass appends another element

      Assumes each appended element is also iterated; the snapshot iterator never sees the appends, so the loop runs exactly twice.

    Explanation

    The copy-on-write list's iterator walks a snapshot of the array as it was when iteration began, so elements appended during the loop are invisible to it — the loop runs a fixed number of times with no fail-fast exception, even though the live list grows. Separately, a successful compare-and-set both checks the expected value and writes the new one, returning true.

  8. Question 8

    What is true of submitting a Callable to an ExecutorService via submit()?

    1. A. It returns the Callable's result directly

      submit() never hands back the result itself; the Future is the handle you redeem later with get().

    2. B. A Callable cannot be submitted; only Runnable is allowed

      submit() is overloaded for both Runnable and Callable, so a Callable is perfectly acceptable; only execute(Runnable) is Runnable-only.

    3. C. It returns a Future whose get() blocks until the task completes and can rethrow the task's exception wrapped in ExecutionExceptionCorrect answer

      submit(Callable) returns immediately with a Future while the task runs asynchronously on a pool thread, and Future.get() blocks until completion, returning the result or rethrowing a failure wrapped in ExecutionException (Javadoc 17: ExecutorService.submit / Future).

    4. D. submit() blocks until the task finishes

      submit() is non-blocking; it is get() that waits for the task to finish.

    Explanation

    submit() schedules a Callable to run asynchronously on a pool thread and returns a Future right away rather than the result itself. The result is redeemed later through Future.get(), which blocks the caller until the task finishes. If the task throws, that exception does not surface raw from get(); it arrives wrapped in an ExecutionException with the original as its cause.

Practise all 18 Concurrency questions

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

Open OCP Java SE 17

Other topics in this pack