What concurrency questions really test
Interviewers want to see that you can spot shared mutable state, explain why it is dangerous, and choose the simplest safe tool: immutability first, then atomics, then locks, then higher-level utilities such as executors and concurrent collections.
- CorrectnessRace conditions, visibility and atomicity.
- LivenessDeadlock, starvation and how to avoid them.
- Modern toolsExecutors, CompletableFuture and virtual threads.
Concurrency questions separate people who have written threaded code from people who have only read about it. Expect a definition, then a scenario: “two threads update the same counter — what goes wrong and how do you fix it?”. The answers below give you both the concept and the fix.
1. What is the difference between a process and a thread?
A process is a running program with its own memory space. A thread is a path of execution inside a process; threads of the same process share the heap but each has its own stack. That shared heap is why threads are cheap to create and why they need synchronisation.
2. What is a race condition? Show one and fix it.
A race condition happens when the result depends on the timing of threads. count++ looks like one step but is three — read, add, write — so two threads can read the same value and one update is lost.
class Counter {
private int count;
void unsafeIncrement() { count++; } // lost updates under load
private final AtomicInteger safe = new AtomicInteger();
void safeIncrement() { safe.incrementAndGet(); } // atomic, lock-free
}3. What does the volatile keyword guarantee — and what does it not?
volatile guarantees visibility: a write by one thread is immediately visible to others, and it prevents reordering around that variable. It does not make compound actions atomic, so volatile int count; count++ is still broken. Use it for flags such as running = false; use atomics or locks for counters.
private volatile boolean running = true;
public void run() {
while (running) { /* work */ }
}
public void stop() { running = false; } // the worker sees this immediately4. synchronized vs ReentrantLock — when would you use each?
synchronized is simple and the lock is released automatically, even on an exception. ReentrantLock adds what synchronized cannot do: tryLock() with a timeout, interruptible waiting, fairness, and several Condition objects. Use synchronized by default and a Lock when you need those extras — always unlocking in finally.
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
transfer(from, to, amount);
} finally {
lock.unlock();
}
}5. What is a deadlock? How do you prevent it?
A deadlock happens when two or more threads each hold a lock the other needs, so none can move. All four Coffman conditions must hold: mutual exclusion, hold and wait, no pre-emption and circular wait. The practical fix is to break circular wait — always acquire locks in the same global order (for example, by account id) — or use tryLock with a timeout.
void transfer(Account a, Account b, int amt) {
Account first = a.id() < b.id() ? a : b; // same order for every thread
Account second = a.id() < b.id() ? b : a;
synchronized (first) {
synchronized (second) { a.debit(amt); b.credit(amt); }
}
}6. Why use an ExecutorService instead of creating threads yourself?
Creating a thread per task is expensive and unbounded — a traffic spike can create thousands of threads and exhaust memory. An ExecutorService reuses a pool of threads, queues extra tasks, returns Future results and shuts down cleanly. In production, size the pool for the work: about the number of cores for CPU-bound tasks, more for I/O-bound tasks.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> total = pool.submit(() -> sumOrders(today));
System.out.println(total.get());
pool.shutdown();7. How does CompletableFuture help with asynchronous code?
CompletableFuture lets you chain async steps without blocking: run tasks in parallel, combine their results and handle errors in one pipeline. It is the Java answer to promises in JavaScript.
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> userApi.find(id));
CompletableFuture<List<Order>> orders = CompletableFuture.supplyAsync(() -> orderApi.forUser(id));
String page = user.thenCombine(orders, (u, o) -> render(u, o))
.exceptionally(ex -> "Please try again")
.join();8. What are virtual threads in Java 21?
Virtual threads (Project Loom) are lightweight threads managed by the JVM rather than the operating system. You can run millions of them, and when one blocks on I/O the JVM parks it and frees the carrier thread. They let you keep simple, blocking, thread-per-request code while scaling like async code. They do not make CPU-bound work faster, and you should avoid holding locks with synchronized around long blocking calls inside them.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String url : urls) {
executor.submit(() -> fetch(url)); // 10,000 calls, no pool tuning
}
}Remember
Key points
- Visibility is not atomicity
volatile fixes visibility; atomics or locks fix compound updates.
- Lock ordering prevents deadlock
Acquire locks in one global order everywhere.
- Pools, not raw threads
ExecutorService bounds resource use and reuses threads.
- Virtual threads for I/O
Great for many blocking calls; no help for CPU-heavy work.
Interview drill
Follow-up questions
What the interviewer asks after your first answer. Turn on practice mode, answer each one aloud, then reveal.
-
1 What is the difference between wait() and sleep()? Basic
sleep()pauses the thread and keeps any locks it holds.wait()must be called inside a synchronised block, releases the monitor and waits until another thread callsnotify(). -
2 Why should you call start() and not run() on a Thread? Basic
start()creates a new thread that then callsrun(). Callingrun()directly just executes the method on the current thread — no concurrency at all. -
3 What is a ThreadLocal and where is it dangerous? Intermediate
It gives each thread its own copy of a variable — handy for request context or date formatters. In thread pools the values outlive the request unless you call
remove(), which leaks data between users and memory.Key insight: Always clear ThreadLocal in a finally block when using pools.
-
4 How do CountDownLatch and CyclicBarrier differ? Intermediate
A
CountDownLatchlets threads wait until a count reaches zero and cannot be reset — for example, wait for three services to start. ACyclicBarriermakes a fixed group of threads wait for each other at a point and can be reused round after round. -
5 Is double-checked locking safe for a singleton? Advanced
Only if the instance field is
volatile; otherwise another thread may see a partly constructed object. The simplest safe singleton is anenumor the holder-class idiom.public final class Config { private Config() { } private static class Holder { static final Config INSTANCE = new Config(); } public static Config get() { return Holder.INSTANCE; } // lazy and thread-safe }