Monitor showing Java code for a concurrency interview
Photo via Unsplash

Java Concurrency Interview Questions: 2026 Guide

Java concurrency is where many backend interviews separate mid-level engineers from senior ones. You can memorize the difference between Runnable and Callable, but interviewers in 2026 want more: can you reason about visibility, spot a race condition in a code review, and explain when virtual threads help and when they don’t? This guide covers the Java concurrency interview questions that come up most often, with the answers and trade-offs interviewers are listening for.

Why Java Concurrency Still Dominates Backend Interviews

Most Java roles involve services that handle many requests at once: REST APIs, Kafka consumers, batch jobs, and caches. A single concurrency bug can corrupt data or stall a production system, so hiring teams test it directly. Expect concurrency questions in three places:

  • Core Java rounds: conceptual questions on threads, locks, and the Java Memory Model.
  • Coding rounds: “make this class thread-safe” or “implement a bounded blocking queue.”
  • Low-level and system design rounds: rate limiters, connection pools, and caches that must behave correctly under load.

Since Java 21 (LTS) made virtual threads final and Java 25 (LTS) followed, interviewers also ask how modern concurrency features change older patterns.

Screen with Java multithreading code
Photo via Unsplash

Core Java Concurrency Interview Questions

1. What is the difference between synchronized and volatile?

volatile guarantees visibility and ordering: a write by one thread is seen by later reads in other threads. It does not make compound actions atomic. synchronized provides both mutual exclusion and visibility, because releasing a monitor establishes a happens-before relationship with the next thread that acquires it.

The classic trap: volatile int count; count++; is still a race, because count++ is read-modify-write. Use AtomicInteger or LongAdder instead.

2. Explain the happens-before relationship

The Java Memory Model (JMM) defines happens-before rules that guarantee one action’s effects are visible to another. Key rules to name: program order within a thread, monitor unlock before subsequent lock, volatile write before subsequent read, Thread.start() before any action in the started thread, and all actions in a thread before another thread returns from join(). Strong candidates explain that without happens-before, the JIT and CPU are free to reorder or cache values.

3. Why is double-checked locking broken without volatile?

public class Config {
    private static volatile Config instance;

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

Without volatile, the reference can be published before the constructor finishes, so another thread may see a partially constructed object. Bonus points for mentioning the holder-class idiom or an enum singleton as simpler alternatives.

4. ReentrantLock vs synchronized: when would you choose each?

Use synchronized by default: it is simpler and cannot be forgotten in a finally block. Reach for ReentrantLock when you need tryLock() with a timeout, interruptible lock acquisition, fairness, or multiple Condition objects. For read-heavy data, mention ReentrantReadWriteLock or StampedLock with optimistic reads.

5. How do you prevent deadlock?

Name the four Coffman conditions (mutual exclusion, hold and wait, no preemption, circular wait) and break at least one. In practice: acquire locks in a consistent global order, use tryLock with timeouts, keep critical sections small, and avoid calling unknown code (callbacks, listeners) while holding a lock. Mention that a thread dump (jstack or jcmd Thread.print) reports detected deadlocks.

Executors, Futures, and Concurrent Collections

6. How do you size a thread pool?

For CPU-bound work, start near the number of cores. For I/O-bound work, a common heuristic is cores × (1 + wait time / compute time). Then measure. Interviewers also want to hear that you would use a bounded queue and an explicit rejection policy in ThreadPoolExecutor, because Executors.newFixedThreadPool uses an unbounded queue that can hide overload until memory runs out.

7. Future vs CompletableFuture

Future.get() blocks. CompletableFuture lets you compose async stages with thenApply, thenCompose, thenCombine, and allOf, and handle failures with exceptionally or handle. A senior-level detail: async methods without an explicit executor run on the common ForkJoinPool, so pass a dedicated executor for blocking I/O.

8. How does ConcurrentHashMap achieve thread safety?

Since Java 8, it uses CAS operations for empty bins and synchronizes on the first node of a bin for updates, so contention is per-bucket rather than map-wide. Reads are generally lock-free. Point out that map.get() followed by map.put() is still a race; use atomic methods like computeIfAbsent, merge, or putIfAbsent.

Server rack representing high-throughput concurrent Java services
Photo via Unsplash

Modern Java: Virtual Threads and Structured Concurrency

9. What are virtual threads and when should you use them?

Virtual threads are lightweight threads managed by the JVM and mounted on a small pool of carrier (platform) threads. When a virtual thread blocks on I/O, it unmounts, freeing the carrier. This makes the simple thread-per-request style scale to very high concurrency without reactive code.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<Order>> futures = orderIds.stream()
        .map(id -> executor.submit(() -> orderClient.fetch(id)))
        .toList();
    // collect results...
}

What interviewers want to hear:

  • Virtual threads help I/O-bound workloads, not CPU-bound ones.
  • Don’t pool them; create one per task.
  • Limit access to scarce resources (database connections, rate-limited APIs) with a Semaphore, not a small thread pool.
  • Be careful with ThreadLocal-heavy code, since millions of threads can mean millions of copies. ScopedValue is the modern alternative.
  • Pinning: in Java 21, blocking inside synchronized could pin the carrier thread. JDK 24 (JEP 491) largely removed that limitation, but native calls can still pin. Knowing this version history signals you keep up to date.

10. What is structured concurrency?

Structured concurrency (StructuredTaskScope, still a preview API in recent JDKs) treats a group of subtasks as a single unit: if one fails, the others are cancelled, and the parent cannot finish before its children. It removes the thread leaks and orphaned tasks common with ad-hoc executors. You don’t need to recite the API, but you should explain the problem it solves.

Coding Round: Implement a Bounded Blocking Queue

This is one of the most common hands-on concurrency questions. A clean answer with ReentrantLock and two conditions:

public class BoundedQueue<T> {
    private final Deque<T> items = new ArrayDeque<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BoundedQueue(int capacity) { this.capacity = capacity; }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (items.size() == capacity) notFull.await();
            items.addLast(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (items.isEmpty()) notEmpty.await();
            T item = items.removeFirst();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Talk through why each detail matters: the while loop guards against spurious wakeups, unlock() sits in finally, and separate conditions avoid waking threads that can’t make progress. Then mention that in production you would simply use ArrayBlockingQueue.

Common Mistakes Candidates Make

  • Saying “just add synchronized” everywhere without discussing contention or lock scope.
  • Using if instead of while around wait() or await().
  • Swallowing InterruptedException. Restore the flag with Thread.currentThread().interrupt() or propagate it.
  • Treating virtual threads as a performance fix for everything, including CPU-heavy work.
  • Ignoring how you’d debug it. Mention thread dumps, JFR (Java Flight Recorder), and stress tests with tools like jcstress.
Developer laptop used for Java concurrency interview practice
Photo via Unsplash

A One-Week Java Concurrency Prep Plan

  1. Day 1: Java Memory Model, volatile, synchronized, happens-before.
  2. Day 2: Locks, conditions, and deadlock scenarios. Write a deadlock, then fix it.
  3. Day 3: ExecutorService, pool sizing, rejection policies.
  4. Day 4: CompletableFuture pipelines and error handling.
  5. Day 5: Concurrent collections and atomics (ConcurrentHashMap, LongAdder).
  6. Day 6: Virtual threads, ScopedValue, and structured concurrency.
  7. Day 7: Timed coding practice: blocking queue, rate limiter, thread-safe LRU cache. Explain your reasoning out loud.

Start Preparing Today

Concurrency questions reward candidates who can explain why, not just what. Pick one question from this guide each day, write the code yourself, and practise explaining the trade-offs out loud as if an interviewer were listening. Mock interviews make a big difference here, because concurrency answers fall apart quickly when you haven’t said them aloud. If you want structured practice, Niraswa AI is one place to start. Open your IDE, build the bounded queue from memory, and get your first rep in today.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *