Java Threads Interview Questions: Concurrency Practice Quiz

Reviewed by Mark Dickie · Last updated

6 questions on this page you can answer and see marked on the spot. Go to the first one

Java threads are lightweight units of execution that let a JVM run multiple tasks concurrently within a single process. A threads-focused interview tests how well you understand the thread lifecycle, synchronization primitives, the java.util.concurrent toolkit, and the Java Memory Model's happens-before guarantees. You should be comfortable explaining synchronized vs ReentrantLock, when to use volatile, how ExecutorService manages thread pools, and what changed with virtual threads in Java 21. Expect questions that ask you to spot race conditions, reason about deadlocks, or choose the right concurrent collection for a given workload.

What does a Java threads interview test?

ConceptWhat to know
Thread lifecycleNEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED states and the transitions between them
Synchronizationsynchronized blocks, ReentrantLock, ReadWriteLock, and when each one applies
Thread poolsExecutorService, the Executors factory methods, pool sizing, and ThreadPoolExecutor tuning
Concurrent collectionsConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue implementations
Coordination utilitiesCountDownLatch, CyclicBarrier, Semaphore, and Phaser
Memory modelvolatile semantics, happens-before ordering, and safe publication of shared fields

How should you prepare for concurrency questions?

  1. Map out the six thread states and draw the transitions between them from memory.
  2. Write small programs that reproduce a race condition, then fix them with synchronized and again with ReentrantLock so you can compare the trade-offs.
  3. Learn the ExecutorService hierarchy cold. Know the difference between newFixedThreadPool, newCachedThreadPool, and newScheduledThreadPool.
  4. Study volatile versus synchronized until you can explain why volatile guarantees visibility but not atomicity for compound actions.
  5. Read up on virtual threads (Java 21, Project Loom) so you can discuss how they change the thread-per-request model and when platform threads are still the right choice.

Key facts

  • Tarmac's Java interview questions cover 18 questions at difficulty 1–5 of 5.
  • Tarmac tracked 1,645 job postings asking for Java in September 2026.
  • Roles asking for Java advertise a median base salary of £80,000, across 352 job postings as of September 2026.
  • Tarmac last reviewed these Java interview questions on 13 October 2026.

At a glance

Questions18
Difficulty1–5 of 5
FormatsMultiple choice, Short answer, True / false, Fill in the blank, Find the bug, Flashcard

What you'll review

  1. threads

Practice questions

Try one before you open the answer. Pick an option and press Check; it's marked on the spot.

In Java, which statement correctly describes how to begin execution of a new thread of execution using the Thread class?#

Options

Show answer

To start a new thread of execution in Java, call start() on the Thread instance. The JVM creates a new OS-level thread and invokes run() inside it. Calling run() directly does not create a new thread — it just executes the method body on the calling thread like any normal method call.

Why:

Calling start() on a Thread instance causes the JVM to create a new OS-level thread and invoke run() within that new thread. Calling run() directly simply executes the method body on the current thread, exactly like any ordinary method call — no new thread is created. Therefore the statement that calling start() on a Thread object creates a new OS-level thread and automatically invokes run() in that thread is correct and the others are wrong.

In Java, which method on a Thread object must you call so that the thread's run() logic executes on a new thread of execution rather than on the caller's current thread?#

Show answer

thread.start()

Why:

Thread.start() allocates and schedules a new thread of execution and, once that thread is running, the JVM invokes the thread's run() method on it. Calling run() directly is an ordinary method call, so it executes on the caller's thread and no new thread is created. The answer is therefore the start() method.

True or false: calling thread.run() directly on a Thread object (instead of thread.start()) starts a new OS thread in Java.#

Options

Show answer

False. Calling Thread.run() directly does not start a new thread in Java. It is an ordinary instance method that executes on the caller's current thread. Only Thread.start() asks the JVM to create a new thread, which then invokes run() asynchronously.

Why:

Thread.run() is a plain instance method. Invoking it directly runs its body on the caller's current thread, just like any other method call, so no new thread is created. Only Thread.start() requests the JVM to create and schedule a new thread, which then calls run() asynchronously. Therefore the statement is false.

Which method on a java.lang.Thread instance creates a new JVM thread of execution and invokes the thread's run() method on that new thread?#

Show answer

start() - calling start() on a Thread creates a new thread of execution, and the JVM then invokes run() on it. Calling run() directly just executes the method body on the caller's thread.

Why:

Calling start() allocates a new native thread and schedules it; the JVM then invokes run() on that new thread. Calling run() directly simply executes the method body on the caller's thread, so no concurrency happens. The question asks for the method name, so an open short answer of start (or start()) is the correct response.

In the Java thread lifecycle, what is the name of the Thread.State enum value for a newly constructed Thread object that has not yet had start() called on it?#

Show answer

NEW. A Thread object is in the NEW state from construction until start() is called, after which it moves to RUNNABLE.

Why:

A Java Thread object is in the NEW state from the moment it is constructed until start() is invoked, at which point it transitions to RUNNABLE. The Thread.State enum has six values: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED.

In Java, a Thread object is created with a Runnable target. What is the essential difference between calling t.start() and calling t.run() directly on that Thread object?#

Options

Show answer

Calling Thread.start() creates a new OS-level thread and the JVM invokes run() on that new thread. Calling run() directly is just a normal method invocation on the current thread — no new thread is created. This distinction matters because directly invoking run() defeats the purpose of using a Thread and makes the code execute sequentially rather than concurrently.

Why:

Thread.start() is the method that actually requests the JVM to allocate a new OS-level thread; the JVM then calls run() on that new thread. If you invoke run() yourself, it is just an ordinary method call on the current thread — no new thread is created, and the code runs inline just like any other method. This is a common beginner mistake that results in code appearing to be multi-threaded when it is actually single-threaded.

What happens when you call start() on a Java Thread object whose thread has already been started and has since terminated? Name the exception thrown, if any, and explain why.#

Show answer

An IllegalThreadStateException is thrown. A Thread instance can be started at most once; calling start() again, whether the thread is still running or has already completed, is illegal because the thread is no longer in the NEW state.

Why:

A Java Thread object maps to one underlying OS thread and can move from NEW to RUNNABLE only once. The JVM enforces this by having start() throw IllegalThreadStateException whenever the thread's internal state is not NEW, and that includes the case where the thread has already finished executing. Calling run() directly would not create a thread at all, and a second start() does not silently restart the thread or throw InterruptedException.

The Java volatile keyword ensures _____ — all threads read the most recently written value of the variable rather than a stale thread-local cache copy — and it establishes happens-before ordering, but it does NOT guarantee _____ for compound read-modify-write operations such as count++.#

Show answer

The Java volatile keyword ensures visibility — all threads read the most recently written value of the variable rather than a stale thread-local cache copy — and it establishes happens-before ordering, but it does NOT guarantee atomicity for compound read-modify-write operations such as count++.

Why:

volatile provides visibility: every read sees the latest write, and a write establishes a happens-before relationship with subsequent reads. It does not make compound operations atomic. count++ involves a read, an add, and a write, which are three separate steps, so two threads can interleave and lose updates even on a volatile field. Atomicity for such operations requires synchronized blocks or an AtomicInteger.

The following Java class implements the double-checked locking singleton pattern. Under the Java Memory Model it is broken: a second thread can observe a non-null instance that points to a partially constructed object. Identify the single line whose defect causes this.#

public class LazyCache {
    private static LazyCache instance;

    public static LazyCache getInstance() {
        if (instance == null) {
            synchronized (LazyCache.class) {
                if (instance == null) {
                    instance = new LazyCache();
                }
            }
        }
        return instance;
    }

    private LazyCache() {}
}
Show answer

The bug is on line 2.

Why:

Line 2 declares instance without the volatile keyword. The statement instance = new LazyCache() is not atomic: it allocates memory, runs the constructor, and assigns the reference. Without volatile, the JIT compiler and CPU may reorder these steps so that the reference is published before construction completes. A thread that bypasses the synchronized block via the first null check can then read a non-null instance pointing to a partially constructed object. Marking the field volatile establishes a happens-before relationship that prohibits this reordering, making the double-checked locking idiom safe. The other lines implement the standard pattern correctly once the field is volatile.

Consider the following class. Thread A calls writer() and Thread B calls reader(), concurrently. Neither field is volatile, and there is no synchronized block, lock, or other synchronization anywhere in the program.#

Options

Show answer

Without a happens-before edge, Java's memory model allows a reader to see ready == true yet still read the stale default 0 for data. Reads cannot return values no thread wrote, such as 17. Guaranteed visibility of data = 42 requires synchronization, such as declaring ready volatile.

Why:

Without a happens-before relationship between the write to data and the read of data, the JMM allows the compiler or processor to reorder the two writes in writer(), so the reader can observe ready == true while still seeing the default 0 for data. The statement that the reader can return 17 is false: no thread ever writes 17, the JMM forbids out-of-thin-air values, and 32-bit int writes are atomic, so a read only returns a value some thread actually wrote (or the default). The statement that the reader is guaranteed to return 42 once it sees ready == true is false for the same reason as the reordering case: the absence of synchronization means no happens-before edge exists to guarantee visibility of data = 42. Declaring ready as volatile would create that edge.

Which of the following scenarios does NOT, by itself, establish a happens-before relationship between two threads in the Java Memory Model (JLS §17.4.5)?#

Options

Show answer

Two threads both reading the same volatile field does not establish a happens-before relationship. The Java Memory Model links a volatile write to a subsequent volatile read, but a read-to-read sequence across threads carries no such guarantee. In contrast, volatile write→read, monitor unlock→lock, and Thread.start()→started-thread actions all create happens-before edges.

Why:

The JMM defines specific happens-before edges: (1) a volatile write happens-before every subsequent volatile read of the same field; (2) a monitor unlock happens-before every subsequent lock on the same monitor; (3) a call to Thread.start() happens-before every action in the started thread. Two threads both reading the same volatile field—even if one read occurs temporally before the other—does not create a happens-before edge, because the volatile guarantee links a write to a subsequent read, never a read to another read. Without an intervening volatile write (or other synchronization action) between the two reads, there is no ordering guarantee.

This BoundedBuffer uses wait() and notifyAll() for blocking semantics. It compiles and passes single-threaded tests, but under concurrent load producers can overflow the buffer and consumers can take from an empty one. Using the 1-based line numbers in the listing, identify every line whose guard must change so the blocking condition is correct. Enter the line numbers.#

1  class BoundedBuffer<T> {
2      private final Object[] items;
3      private int putIndex = 0;
4      private int takeIndex = 0;
5      private int count = 0;
6
7      BoundedBuffer(int capacity) {
8          items = new Object[capacity];
9      }
10
11     public synchronized void put(T item) throws InterruptedException {
12         if (count == items.length) {
13             wait();
14         }
15         items[putIndex] = item;
16         putIndex = (putIndex + 1) % items.length;
17         count++;
18         notifyAll();
19     }
20
21     public synchronized T take() throws InterruptedException {
22         if (count == 0) {
23             wait();
24         }
25         T item = (T) items[takeIndex];
26         items[takeIndex] = null;
27         takeIndex = (takeIndex + 1) % items.length;
28         count--;
29         notifyAll();
30         return item;
31     }
32 }
Show answer

The bug is on lines 12 and 22.

Why:

The wait() calls on lines 13 and 23 release the monitor and park the thread until notified, but a waiting thread can return from wait() without any notification (spurious wakeup, which Object.wait's documentation explicitly allows). Also, notifyAll() wakes every waiter, and only one can hold the monitor at a time, so by the time a later waiter runs, the condition it was waiting for may already be false again. Because the guards on lines 12 and 22 are if statements, the thread proceeds immediately after wait() returns without re-checking the condition, so a producer can write into a full buffer or a consumer can read from an empty one. Changing both if statements to while loops makes each thread re-test its condition after every wakeup and wait again if it still does not hold, which is the standard guarded-wait pattern. The fix is therefore to change lines 12 and 22 and nothing else in this listing.

Consider the following class, where Thread A calls writer() and Thread B calls reader() on the same Shared instance. The JLS happens-before rules from the Java Memory Model (JLS §17.4) apply. If Thread B's reader() observes ready == true at the point marked (3), what value of x is guaranteed to be printed at (4)? Explain why, naming the happens-before edges that make this guarantee.#

Show answer

The value printed at (4) is guaranteed to be 1. Program order in Thread A makes the write x = 1 at (1) happen-before the volatile write ready = true at (2). By the volatile rule (JLS §17.4.4), that volatile write happens-before any subsequent read of ready that observes true, so it happens-before the read at (3) in Thread B. Program order in Thread B makes the read of ready at (3) happen-before the read of x at (4). Because happens-before is transitive, the write at (1) happens-before the read at (4), so Thread B is guaranteed to see x = 1. The volatile write/read pair acts as a release/acquire, publishing all of Thread A's prior writes, not just the volatile field itself. Without the volatile modifier on ready, no such edge would exist and x could be observed as 0.

Why:

The guaranteed value is 1. Three happens-before edges chain together: program order in Thread A orders the write x = 1 before the volatile write ready = true; the volatile rule orders that volatile write before the volatile read at (3) because the read observed true; and program order in Thread B orders the read at (3) before the read of x at (4). Transitivity of happens-before then guarantees the write to x is visible at (4). The common misconception that x could still be 0 because x is not volatile confuses field-level visibility with the broader guarantee that a volatile write publishes all of the writing thread's prior writes.

Consider the following Java program (Java 9+, using VarHandle access modes). Two threads share a Demo instance whose fields a and c both default to 0. Thread t1 performs a plain write to a followed by a release write to c. Thread t2 performs an acquire read of c, then a plain read of a, and prints c,a.#

Options

Show answer

The output 1,0 can never occur. When the acquire read of c returns 1, the release-acquire pairing establishes a happens-before edge from the plain write a=1 (before the release write) to the plain read of a (after the acquire read), guaranteeing a is visible as 1. If c reads 0, no edge exists and a may be 0 or 1, so 0,0 and 0,1 are both legal.

Why:

A release write (setRelease) and an acquire read (getAcquire) form a release-acquire pair: if the acquire read observes the value written by the release write, a happens-before edge is established from the release write to the acquire read. In t1, the plain write A.set(d, 1) is ordered before C.setRelease(d, 1) by program order, and the release mode prevents the plain write from being reordered after the release write. In t2, the acquire read C.getAcquire(d) is ordered before the plain read A.get(d) by program order, and the acquire mode prevents the plain read from being reordered before the acquire read. Therefore, if C.getAcquire returns 1, transitivity gives: A.set(d,1) → C.setRelease(d,1) → C.getAcquire(d) → A.get(d), so A.get is guaranteed to return 1 — the output 1,0 is impossible. If C.getAcquire returns 0, no happens-before edge is created between the threads, and the plain read A.get may observe either the default 0 or t1's plain write of 1, making 0,0 and 0,1 both legal. 1,1 is obviously legal when the acquire read sees the release write.

Consider this Java class, shared by two threads:#

Show answer

Yes, the printed value is guaranteed to be 10. In Thread A, the last count++ (writing 10) is sequenced before the volatile write done = true in markDone() by program order. A volatile write happens-before any subsequent volatile read of the same field that observes it, so that write happens-before Thread B's volatile read of done in check() when it returns a value other than -1. Inside check(), that volatile read is sequenced before the read of count. By transitivity, the write of 10 to count happens-before Thread B's read of count, so Thread B sees 10. The synchronized on increment() does not provide this link, because check() never acquires that monitor; the volatile field is the sole cross-thread happens-before bridge.

Why:

The guarantee follows from tracing a full happens-before chain defined by the Java Memory Model (JLS 17.4.5). In Thread A, the final count++ (which writes 10) is sequenced before the volatile write done = true by program order. A volatile write happens-before every later volatile read of the same field that observes its value, so when Thread B's check() reads done as true, that edge exists. Within check(), the volatile read of done is sequenced before the read of count. Transitivity gives: write of count = 10 (Thread A) happens-before volatile write of done (Thread A) happens-before volatile read of done (Thread B) happens-before read of count (Thread B). Therefore the printed value is guaranteed to be 10. The synchronized block in increment() does not create this guarantee for Thread B, since a monitor release only synchronizes with a later acquire of the same monitor and check() never acquires it. The volatile field is the only bridge between the threads.

Consider the following Java code using synchronized blocks and the happens-before relationship defined by the Java Memory Model (JLS §17.4.4–17.4.5). Two threads share the same monitor object lock (an instance of a final field). Thread A executes lock's synchronized block, setting x = 10, then releases the lock. Later, Thread B enters lock's synchronized block and reads x with no other synchronization in between. Explain the complete chain of reasoning by which the Java Memory Model guarantees Thread B will read x as 10 rather than a stale or default value.#

Show answer

When Thread A exits the synchronized block on lock, it releases the monitor lock. The JLS defines that every action on a monitor lock happens-before every subsequent lock of that same monitor. Thread A's write x = 10 is program-ordered before its release of lock, so by the program-order happens-before within Thread A, that write happens-before the unlock action. The unlock action by Thread A happens-before Thread B's subsequent lock action on the same monitor, per the monitor-lock happens-before rule (JLS §17.4.5). Thread B's lock action happens-before its read of x by program order within Thread B. By transitivity of happens-before (if a happens-before b and b happens-before c, then a happens-before c), the write x = 10 in Thread A happens-before the read of x in Thread B. Therefore the read is guaranteed to see the value 10, because a read of a non-volatile field sees the result of the last write that happens-before that read in the happens-before partial order.

Why:

The Java Memory Model builds a happens-before partial order from three rules. Within Thread A, program order gives a happens-before edge from the write x = 10 to the unlock of lock. The monitor-lock rule (JLS §17.4.5) states that every unlock of a monitor happens-before every subsequent lock of that same monitor, connecting Thread A's unlock to Thread B's lock. Within Thread B, program order gives an edge from the lock acquisition to the read of x. By transitivity of happens-before, the write in Thread A happens-before the read in Thread B, so the read is guaranteed to observe the value 10 and not a stale or default value.

In lock-free programming using compare-and-swap (CAS), what is the ABA problem, and how does a versioned/tagged pointer or hazard pointer mitigate it?#

Show answer

The ABA problem: a thread reads value A from a shared location, another thread changes it to B and then back to A, and the first thread's CAS succeeds because it only compares the current value with the expected value, so the intervening changes go unnoticed. This is dangerous when the node at that address was freed and reallocated in between. A versioned/tagged pointer stores a monotonically increasing counter alongside the pointer and increments it on every successful update; the CAS compares pointer and tag together, so it fails after an ABA sequence even though the pointer looks unchanged. A hazard pointer has each thread publish the address of the node it is about to access; reclamation checks these published pointers before freeing or reusing memory, so a node cannot be recycled to the same address while another thread still holds a reference to it, which removes the address reuse that lets ABA corrupt the structure.

Why:

CAS only tests whether the current value equals the expected value, so a location that goes A to B to A looks unchanged to a thread that read it earlier. The danger is strongest when the value is a pointer to a node that was freed and reallocated at the same address between the two observations. A versioned/tagged pointer addresses the comparison itself: the tag changes on every write, so a CAS that compares pointer and tag together detects the intervening mutations. A hazard pointer does not change the CAS comparison; instead it publishes which nodes threads are still reading, and reclamation defers reuse of those nodes, so the reallocated-at-same-address scenario cannot occur while a reader holds the reference.

Consider the following Java class, where init() is called by Thread A and check() is called by Thread B on the same Service instance. Thread B's volatile read at R1 observes ready == true. Which statement is correct about the value printed at R2, and why under the Java Memory Model (JLS §17.4)? Assume no other threads access this object and int writes are atomic per JLS §17.7. The relevant happens-before rules are: (i) each action in a thread happens-before every subsequent action in that same thread's program order; (ii) a write to a volatile field happens-before every subsequent read of that same field that observes the written value; and happens-before is transitively closed. Use the line labels in the code below to trace edges through the code and identify whether a happens-before path exists from W2 to R2.#

Show answer

The value may be 0 or 42 with no guarantee under the JMM: W1 (ready=true) happens-before R1 (volatile read) and R1 happens-before R2 (program order), but W2 (value=42) is after W1 in Thread A's program order, so there is no happens-before path from W2 to R2.

Why:

Tracing the happens-before edges through the code: (1) Program order in Thread A gives W1 → W2. (2) Program order in Thread B gives R1 → R2. (3) The volatile rule gives W1 → R1, because W1 is the write to the volatile field ready and R1 is a subsequent read of ready that observes the value true. By transitivity, W1 → R1 → R2, so W1 happens-before R2. However, W2 (the write to value) appears AFTER W1 in Thread A's program order — the edge W1 → W2 points away from W2, not toward R2. There is no path in the happens-before graph from W2 to R2. Because the write to the non-volatile field value at W2 does not happen-before the read of value at R2, the JMM provides no guarantee that R2 will observe the value 42 written by W2. The only values ever written to value are the default 0 and 42 (at W2), so R2 may return either 0 or 42, with no guarantee as to which. This contrasts with the safe pattern where value = 42 precedes ready = true: then W1 (value=42) → W2 (ready=true) → R1 → R2, giving W1 → R2 and guaranteeing R2 sees 42.

Related interview questions

Job market

See java salaries and hiring demand from live job postings.

Practise these until they stick

That's every question we hold on this topic, and the page marks what you pick. What it can't do is remember. A free account keeps every answer, and what you miss comes back until it's right: after a day, then at longer gaps.

Start with this topic

Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan

Interview booked? Practise the job ad instead

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.