Skip to content
Java interview 9 min read

Concurrency Interview Questions

Java concurrency is one of the most challenging and heavily tested topics in technical interviews. This page walks through the most important questions — from classic threading basics to modern java.util.concurrent APIs — with clear, accurate answers and illustrative code.

Fundamentals

Q1: What is the difference between a process and a thread?

A process is an independent program instance with its own memory space. A thread is a lightweight unit of execution that lives inside a process and shares the process’s heap and static memory with other threads in the same process, while each thread owns its own stack and program counter.

AspectProcessThread
MemoryIsolated address spaceShared heap with siblings
Creation costHeavy (OS fork/exec)Light (stack allocation only)
CommunicationIPC (pipes, sockets…)Shared variables (needs synchronization)
Failure isolationCrash stays containedOne thread can crash the JVM

Q2: What are the thread states in Java?

Java threads cycle through six states defined in Thread.State:

public class ThreadStates {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try { Thread.sleep(500); } catch (InterruptedException e) {}
        });

        System.out.println(t.getState()); // NEW
        t.start();
        System.out.println(t.getState()); // RUNNABLE
        t.join();
        System.out.println(t.getState()); // TERMINATED
    }
}

The full lifecycle is covered in detail on the Thread Life Cycle page.

Q3: What is the difference between run() and start()?

Calling run() directly executes the thread body on the calling thread — no new thread is created. Only start() tells the JVM to spawn a new OS thread and then invoke run() on it. This is a common beginner trap; see run() vs start() for more.


Synchronization & Visibility

Q4: What does synchronized mean and what does it guarantee?

The synchronized keyword acquires a monitor lock (also called an intrinsic lock) on an object before executing the protected block, and releases it on exit — even if an exception is thrown. It provides two guarantees:

  1. Mutual exclusion — only one thread holds the lock at a time.
  2. Memory visibility — changes made inside a synchronized block are visible to any thread that subsequently acquires the same lock.
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++; // read-modify-write is now atomic
    }

    public synchronized int get() {
        return count;
    }
}

Warning: synchronized on different objects does NOT protect the same data. Always synchronize on the same lock to guard the same resource.

Q5: What is volatile and when should you use it?

volatile guarantees visibility but not atomicity. Every write to a volatile variable is flushed to main memory immediately, and every read fetches the latest value from main memory, bypassing CPU caches.

public class StopFlag {
    private volatile boolean running = true;

    public void stop() { running = false; }

    public void run() {
        while (running) {
            // do work
        }
        System.out.println("Stopped.");
    }
}

Without volatile, the worker thread might cache running in a register and loop forever. Use volatile for simple flags or state variables where only a single write/read operation is needed. For compound actions (check-then-act, increment), use synchronized or AtomicInteger instead.

See the volatile Keyword page and Java Memory Model for deeper coverage.

Q6: What is the difference between synchronized and ReentrantLock?

Both achieve mutual exclusion, but ReentrantLock gives you more control:

FeaturesynchronizedReentrantLock
Reentrant?YesYes
Try-lock (non-blocking)NotryLock()
Timed lock attemptNotryLock(time, unit)
Interruptible lock waitNolockInterruptibly()
Fairness policyNonew ReentrantLock(true)
Multiple conditionsNolock.newCondition()
Must release manuallyN/AYes — use finally
import java.util.concurrent.locks.ReentrantLock;

public class SafeCounter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // always in finally
        }
    }
}

Tip: Prefer synchronized for simple cases — it’s less error-prone. Reach for ReentrantLock when you need tryLock, timed waits, or multiple condition variables.

Full details are on the ReentrantLock & Monitors page.


Deadlock, Livelock & Starvation

Q7: What is a deadlock? How do you prevent it?

A deadlock occurs when two or more threads each hold a lock the other needs, forming a cycle where none can proceed.

// Classic deadlock scenario
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        synchronized (lockB) { /* work */ }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lockB) { // acquires B first — opposite order!
        synchronized (lockA) { /* work */ }
    }
});

Prevention strategies:

  • Lock ordering — always acquire locks in the same global order.
  • tryLock with timeout — back off and retry if you can’t acquire all locks.
  • Lock-free data structures — use java.util.concurrent.atomic classes.
  • Minimize lock scope — hold locks for the shortest time possible.

See Deadlock for a full walkthrough.

Q8: What is the difference between deadlock, livelock, and starvation?

  • Deadlock — threads are blocked forever waiting on each other.
  • Livelock — threads are actively running but keep reacting to each other and making no real progress (like two people stepping aside for each other in a hallway, indefinitely).
  • Starvation — a thread is perpetually denied CPU time or a lock because higher-priority threads keep taking it.

The Java Memory Model

Q9: What is the Java Memory Model (JMM)?

The JMM defines the rules under which one thread’s writes become visible to another thread’s reads. Without it, the JVM and CPU are free to reorder instructions and cache values — which is great for performance but can break multi-threaded code.

The key concept is a happens-before relationship. If action A happens-before action B, then B is guaranteed to see A’s effects. Happens-before edges are created by:

  • A thread calling start() on another thread.
  • A thread’s join() returning.
  • Releasing a monitor (synchronized exit) and a subsequent acquisition of the same monitor.
  • Writing to a volatile variable and a subsequent read of that same variable.

See the full Java Memory Model page for more.

Q10: What is a race condition?

A race condition occurs when the correctness of a program depends on the relative timing of thread execution — and that timing is not controlled. The classic example is a non-atomic check-then-act:

// UNSAFE — two threads can both pass the if-check before either decrements
if (count > 0) {
    count--;
}

// SAFE — use AtomicInteger or synchronize
AtomicInteger atomicCount = new AtomicInteger(10);
if (atomicCount.getAndDecrement() > 0) {
    // safely consumed one unit
}

java.util.concurrent Utilities

Q11: What is the Executor framework and why use it?

Creating raw threads is expensive and hard to manage at scale. The Thread Pools & Executors framework from java.util.concurrent decouples task submission from execution mechanics.

import java.util.concurrent.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

for (int i = 0; i < 10; i++) {
    final int taskId = i;
    pool.submit(() -> System.out.println("Task " + taskId + " on " + Thread.currentThread().getName()));
}

pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);

Q12: What is Callable vs Runnable? What is Future?

  • Runnable.run() returns void and cannot throw checked exceptions.
  • Callable<V>.call() returns a result of type V and can throw checked exceptions.
  • Future<V> represents the eventual result of a Callable. Call future.get() to block until the result is ready.
ExecutorService exec = Executors.newSingleThreadExecutor();

Future<Integer> future = exec.submit(() -> {
    Thread.sleep(200);
    return 42;
});

System.out.println("Result: " + future.get()); // blocks until done
exec.shutdown();

Output:

Result: 42

Details on Callable & Future.

Q13: What are atomic variables?

java.util.concurrent.atomic provides lock-free thread-safe operations on single variables using CPU-level Compare-And-Swap (CAS) instructions.

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic, no lock needed
counter.compareAndSet(1, 2); // CAS: set to 2 only if current value is 1

Common classes: AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference<V>.

Q14: What is CountDownLatch and when would you use it?

CountDownLatch lets one or more threads wait until a set of operations in other threads completes. You initialize it with a count; worker threads call countDown(), and the waiting thread calls await().

import java.util.concurrent.*;

CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " done");
        latch.countDown();
    }).start();
}

latch.await(); // main thread waits for all 3
System.out.println("All workers finished.");

Note: A CountDownLatch cannot be reset. Use CyclicBarrier if you need a reusable barrier that resets after each cycle.

Q15: What is ConcurrentHashMap and how is it different from Hashtable?

Both are thread-safe maps, but they differ significantly in performance:

FeatureHashtableConcurrentHashMap
Lock scopeEntire mapPer-bucket (segment-level in Java 7, node-level in Java 8+)
null keys/valuesNot allowedNot allowed
Read operationsLockedLock-free (read without locking)
PerformanceBottlenecks under contentionScales well with many threads
IteratorFail-fastWeakly consistent (does not throw ConcurrentModificationException)

See Concurrent Collections for the full picture.


Under the Hood

How JVM Synchronization Works

When a thread enters a synchronized block, the JVM executes a monitorenter bytecode instruction, attempting to acquire the object’s monitor. If another thread holds it, the requesting thread is parked (moved to the monitor’s wait set) and de-scheduled by the OS.

In modern JVMs (HotSpot), monitors are optimized in three tiers:

  1. Biased locking — if only one thread ever accesses the object, the lock is “biased” toward it and future re-acquisitions cost nearly zero (removed in Java 21 as it added JVM complexity).
  2. Thin lock (stack lock) — if a second thread competes, a CAS on the object header is used — still very fast.
  3. Fat lock (inflated monitor) — if contention persists, a full OS mutex is used.

This tiering means that lightly-contended synchronized code is far faster than its reputation suggests. Benchmark before switching to ReentrantLock for performance reasons alone.

Virtual Threads and Concurrency (Java 21)

Virtual Threads, introduced as a preview in Java 19 and made stable in Java 21, are lightweight threads managed by the JVM rather than the OS. They allow you to write blocking code that scales like async code — you can have millions of virtual threads without exhausting OS thread limits.

// Java 21: spawn a virtual thread
Thread.ofVirtual().start(() -> System.out.println("Virtual thread running"));

For CPU-bound work, platform threads + thread pools remain the right tool. Virtual threads shine for I/O-heavy workloads.


Quick-Reference: Most Common Pitfalls

  • Forgetting volatile on a flag — the reading thread may never see the update.
  • Using ++ on a shared int without synchronization — not atomic.
  • Calling wait()/notify() outside a synchronized block — throws IllegalMonitorStateException.
  • Not calling unlock() in a finally block — leaves locks permanently held on exceptions.
  • Calling start() twice — throws IllegalThreadStateException; see Starting a Thread Twice.

Last updated June 13, 2026
Was this helpful?