1. Java Memory Model (JMM) & Thread Safety Fundamentals
Volatile, Atomicity, Visibility, and the Happens-Before Relationship
The Java Language Specification defines the Java Memory Model's allowed visibility and ordering behaviors; it does not prescribe a particular hardware implementation. Interview answers should describe those guarantees rather than assume one mechanism. JLS Chapter 17
Core Concepts You Must Explain:
- Visibility: When Thread A updates a shared variable, Thread B may continue reading the stale value from its local CPU register or cache line unless a memory barrier is established.
- Atomicity: Operations like
count++ look atomic but actually compile to three bytecode instructions: read, modify, write. Without synchronization, increments will be lost. - Ordering (Instruction Reordering): Both the Java compiler and the CPU reorder instructions for maximum instruction pipeline efficiency as long as single-threaded semantics are preserved.
volatile vs synchronized vs AtomicInteger
volatile: Guarantees visibility and ordering (via memory fence preventing instruction reordering), but does NOT guarantee atomicity.synchronized: Guarantees visibility, atomicity, and mutual exclusion by acquiring the intrinsic object monitor lock.AtomicInteger / AtomicReference: Uses CPU hardware-level Compare-And-Swap (CAS) instructions (e.g. LOCK CMPXCHG on x86) for non-blocking, lock-free atomicity.
Double-Checked Locking Singleton Pattern (Correct with volatile)public class ThreadSafeSingleton {
// Crucial: volatile prevents instruction reordering during instantiation!
private static volatile ThreadSafeSingleton instance;
private ThreadSafeSingleton() {}
public static ThreadSafeSingleton getInstance() {
if (instance == null) { // First check (no lock penalty)
synchronized (ThreadSafeSingleton.class) {
if (instance == null) { // Second check (under lock)
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
}
3. ThreadPoolExecutor Internals & Production Sizing
How task queues, core pools, and rejection policies really work
Many candidates fail to explain the lifecycle of a task submitted to ThreadPoolExecutor.
Task Execution Order:
- If running threads <
corePoolSize, executor spawns a new worker thread immediately. - If running threads >=
corePoolSize, executor puts the task into the WorkQueue (e.g. ArrayBlockingQueue or LinkedBlockingQueue). - Only when the queue is FULL does the executor spawn threads up to
maximumPoolSize! - If threads ==
maximumPoolSize and the queue is full, the RejectedExecutionHandler triggers.
Saturation Rejection Policies:
AbortPolicy (Default): Throws RejectedExecutionException.CallerRunsPolicy: Runs the task on the submitting thread (naturally throttles producer).DiscardPolicy: Silently drops the task.DiscardOldestPolicy: Discards the oldest unhandled task in the queue and retries.
- CPU-Bound Tasks: Optimal Threads = $\text{Number of CPU Cores} + 1$.
- I/O-Bound Tasks (DB/HTTP calls):
$\text{Optimal Threads} = \text{CPU Cores} \times \left(1 + \frac{\text{Wait Time}}{\text{Compute Time}}\right)$
5. Virtual Threads (Project Loom) in Java
Virtual threads for high-concurrency, mostly waiting workloads
Virtual threads became a permanent Java feature in JDK 21. They are useful to understand when evaluating thread-per-task designs; interview topics vary by role, team, and JDK version. OpenJDK JEP 444
- Platform threads: typically map to OS threads; achievable counts depend on operating system, runtime settings, and available resources.
- Virtual threads: scheduled by the JDK over a smaller set of platform threads (M:N). They can make large numbers of mostly waiting tasks more practical, but feasible counts depend on workload, stack depth, heap, native resources, and the runtime.
How Virtual Threads Work:
- When a virtual thread executes blocking I/O (e.g.
Socket.read(), Thread.sleep()), the JVM automatically unmounts the virtual thread from its carrier thread and parks its continuation in heap memory. - The carrier OS thread immediately processes another runnable virtual thread.
- When the I/O event completes, the JVM remounts the virtual thread onto an available carrier thread!
Interview Warning: The Pinning Trap!
- In JDK 21–23, blocking in some
synchronized constructs could pin a virtual thread to its carrier; native/foreign calls also have version-dependent constraints. JDK 24 changed monitor behavior so virtual threads can unmount while blocked in synchronized code. Oracle JDK 24 migration guide - Diagnose pinning in the target JDK and workload before changing synchronization primitives; replacing
synchronized with ReentrantLock is not a blanket requirement.
Launching Virtual Threads using Java Structured Concurrencytry (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // Does NOT block OS thread!
return i;
});
});
} // Automatically waits for all 10,000 virtual threads to finish!
Frequently Asked Questions
What is the difference between volatile and synchronized in Java?
Under the Java Memory Model, volatile reads and writes have defined visibility and ordering effects but do not make compound operations such as count++ atomic. synchronized provides mutual exclusion and happens-before relationships; its runtime costs depend on contention and implementation.
Why does ConcurrentHashMap not allow null keys or null values?
In a concurrent environment, if map.get(key) returned null, you could not distinguish whether the key was absent or the key was mapped to null without calling map.containsKey(key). In multithreaded systems, the key could be inserted or removed between the two calls, creating a critical race condition.
What is thread pinning in Java Virtual Threads?
Pinning behavior depends on the JDK release and blocking operation. In JDK 21–23, certain monitor and native/foreign interactions could pin a virtual thread; JDK 24 changed monitor behavior. Check the documentation and diagnostics for the deployed JDK before changing locks.
What is the difference between CountDownLatch and CyclicBarrier?
A CountDownLatch cannot be reset once its count reaches zero; it is used when one or more threads wait for N operations to complete. A CyclicBarrier can be reset and reused after all waiting threads are released, making it ideal for iterative parallel algorithms where threads sync at barrier points.
How does the Happens-Before relationship work in the Java Memory Model?
The happens-before relationship guarantees that memory writes by one thread are visible to subsequent reads by another thread. Critical rules include: monitor unlock happens-before subsequent lock on the same monitor, write to volatile variable happens-before subsequent read of that variable, and Thread.start() happens-before any action in the started thread.
What happens when the work queue in a ThreadPoolExecutor is full?
When the task queue is full and active threads are less than maximumPoolSize, the executor allocates additional worker threads up to maximumPoolSize. If the active threads already equal maximumPoolSize, the configured RejectedExecutionHandler (AbortPolicy, CallerRunsPolicy, DiscardPolicy, or DiscardOldestPolicy) is invoked.