Java Memory Model, Happens-Before & Race Conditions
To write correct, high-performance concurrent code in Java, engineers must look past surface-level myths ("threads cache variables in L1") and understand how the Java Memory Model (JMM, JLS §17.4) governs compiler optimizations (C2 JIT), CPU store buffers, instruction reordering, and Happens-Before edges.
1. The Java Memory Model (JMM) & The L1 Cache Myth
The JMM is a formal contract between the Java programmer, the JIT compiler (javac / C1 / C2), and the underlying CPU hardware (registers, Store Buffers, L1/L2/L3 caches, and RAM).
The CPU Cache Visibility Problem
Access Latency: N/A (Architecture Flaw)
Overview: Why Thread 1 changes are invisible to Thread 2 without synchronization.
- Under-The-Hood Details:
- Core 0 writes variable x = 42. The change is buffered in Core 0's write buffer and L1 cache.
- Core 1 reads x from its own L1 cache which still contains the stale value (x = 0).
- Fix: Declaring a variable volatile or using synchronized inserts a memory fence (barrier) that forces Core 0 to flush its store buffer and Core 1 to invalidate its local caches.
💡 Click on CPU Cores, L3 Shared cache, Main RAM, or the stale read line above to diagnose JMM Visibility Bugs.
Refuting the "Stale L1 Cache" Myth: C2 JIT Hoisting vs. MESI
Consider the classic infinite loop bug when stopping a worker thread without volatile:
public class StopFlagBug {
private static boolean stop = false; // Missing volatile!
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long i = 0;
while (!stop) {
i++;
}
System.out.println("Stopped at i = " + i);
});
worker.start();
Thread.sleep(1000);
stop = true;
System.out.println("Main set stop = true");
}
}
A common textbook misconception claims this program hangs because "Core 1 caches stop = false in its L1 cache and never syncs with Core 0." On modern x86-64 and ARMv8 CPUs, hardware cache coherence protocols (MESI / MOESI / MOESIF) automatically invalidate stale L1 cache lines across cores within nanoseconds.
Why does the program actually hang forever? C2 JIT Loop-Invariant Code Motion (Hoisting).
- During the first ~10,000 iterations, the worker thread runs in the interpreter (
-Xint) or C1 compiler. - Once the loop becomes hot, the C2 JIT compiler analyzes
while (!stop) { i++; }. Becausestopis notvolatileand nothing inside the loop body modifiesstopor establishes a synchronization edge, the JMM allows C2 to assume no other thread modifiesstop. - C2 hoists the read out of the loop into a CPU register:
if (!register_stop) {while (true) { // Infinite loop in native assembly!i++;}}
- Empirical Proof:
- Running with
java -Xint StopFlagBug(disabling JIT) orjava -XX:TieredStopAtLevel=1 StopFlagBug(C1 only, no C2 hoisting) causes the worker thread to terminate immediately afterstop = truebecause MESI cache coherence propagates the cache line update! - Adding
System.out.println(i)orThread.sleep(0)inside the loop also makes the loop terminate—not because of a valid synchronization contract onstop, but becausePrintStream.printlnacquires an internal lock / barriers andThread.sleepcrosses a native syscall boundary, preventing C2 from hoistingstop. Relying on side-effects inside a loop without a direct happens-before edge on the variable itself is a Heisenbug that can break across JDK upgrades.
- Running with
With no method calls, locks, or volatile barriers inside the loop, HotSpot C2 proves stop is not modified locally and hoists the read outside the loop. Even though MESI cache coherence updates L1 cache in nanoseconds, the CPU never reads memory again!
2. Store Buffers, Hardware Reordering & x86 TSO vs. ARMv8
If MESI keeps L1 caches coherent, when does the CPU hardware itself cause visibility and ordering bugs? The answer lies in Store Buffers, Invalidate Queues, and Out-of-Order Execution.
The Dekker / Store Buffering Litmus Test (jcstress)
Consider two threads executing on two cores with initial state x = 0, y = 0 (non-volatile):
// Thread 1 (Core 0) // Thread 2 (Core 1)
x = 1; y = 1;
int r1 = y; int r2 = x;
Intuitively, if Thread 1 runs first (r1 = 0, r2 = 1), Thread 2 runs first (r1 = 1, r2 = 0), or they interleave (r1 = 1, r2 = 1), it seems impossible to get r1 = 0, r2 = 0. Yet running this test with OpenJDK jcstress (@JCStressTest) on an Intel/AMD x86-64 processor yields r1 = 0, r2 = 0 on ~6.29% of executions!
Physical Mechanism:
- Core 0 executes
x = 1. Instead of stalling ~50–100 cycles waiting for Core 1 to acknowledge the MESI Read-For-Ownership (RFO) invalidation, Core 0 writesx = 1into its private Store Buffer and immediately executesr1 = y(readingy = 0from L1 cache). - Simultaneously, Core 1 writes
y = 1into its private Store Buffer and executesr2 = x(readingx = 0from L1 cache). - Both Store Buffers subsequently drain to L1 cache and invalidate the peer core's cache line—too late! To the outside world, the Store-Load operations were reordered.
x86-64 (TSO) vs. ARMv8 / Apple Silicon / AWS Graviton (Weakly Ordered)
Not all CPU architectures allow the same hardware reorderings. This distinction explains why buggy concurrent Java code can run for years on Intel servers and crash within minutes after migrating to AWS Graviton (ARM64):
| Reordering Type | Pattern | x86-64 (Total Store Order) | ARMv8 / AArch64 (Weak Memory Model) | Compiler (C2 JIT) |
|---|---|---|---|---|
| Store-Load | Write x=1 Read y | Allowed (via Store Buffer) | Allowed | Allowed |
| Load-Load | Read x Read y | Forbidden in hardware | Allowed | Allowed |
| Load-Store | Read x Write y=1 | Forbidden in hardware | Allowed | Allowed |
| Store-Store | Write x=1 Write y=1 | Forbidden in hardware | Allowed | Allowed |
[!WARNING] Why Unsafe Double-Checked Locking (DCL) Explodes on ARMv8: In
instance = new Helper(), allocating memory, running the constructor (helper.field = 42), and assigning the referenceinstanceare separate stores. On x86-64, hardware forbidsStore-Storereordering, often masking a missingvolatileunless JIT reorders it. On ARMv8 (AWS Graviton / Apple M-series), the CPU itself reorders the reference assignment before the constructor store completes (Store-Store+Load-Load), causing another thread to observe a non-nullinstancewith uninitialized default fields (0/null)!
JVM Memory Barriers (JMM Cookbook)
To enforce JMM guarantees across any CPU architecture, the JVM emits logical memory barriers (implemented as lock addl $0, 0(%rsp) or mfence on x86, and dmb ish / ldar / stlr on ARMv8):
LoadLoad: Ensures loads before the barrier complete before loads after the barrier.StoreStore: Ensures stores before the barrier are visible before stores after the barrier (flushes Store Buffer before publishing reference).LoadStore: Ensures loads before the barrier complete before stores after the barrier.StoreLoad: The heaviest barrier! Drains the Store Buffer completely before any subsequent load executes (required after everyvolatilewrite followed by avolatileread).
3. The Happens-Before Relationship (All 8 JLS §17.4.5 Rules)
Instead of forcing developers to reason about x86 Store Buffers versus ARMv8 pipeline reorderings, the Java Language Specification defines the Happens-Before () partial ordering.
The Fundamental Guarantee: If action happens-before action (), then every memory write performed by is guaranteed to be visible to , and neither the JIT compiler nor the CPU may reorder after . If two conflicting accesses (at least one is a write) have no happens-before edge between them, the program contains a Data Race.
The Complete List of Happens-Before Rules
- Program Order Rule: Within a single thread, each action happens-before every subsequent action in program order ().
- Monitor Lock Rule: An unlock (
synchronizedexit orLock.unlock()) on a monitor happens-before every subsequent lock acquisition on that same monitor instance. - Volatile Variable Rule: A write to a
volatilefield happens-before every subsequent read of that samevolatilefield. - Thread Start Rule: A call to
Thread.start()happens-before any action inside the started thread. - Thread Termination / Join Rule: Any action in a thread happens-before another thread returns from
Thread.join(),Thread.isAlive() == false, orFuture.get()on that thread. - Thread Interruption Rule: A thread calling
t.interrupt()happens-before the interrupted thread detects the interrupt (viaInterruptedException,interrupted(), orisInterrupted()). - Finalizer Rule: The end of an object's constructor happens-before the start of its
finalize()method. - Transitivity Rule: If and , then .
Transitive Piggybacking in Practice
Because of Program Order + Volatile/Monitor + Transitivity, you do not need to make every field volatile. If Thread 1 writes to plain fields and then writes to a single volatile variable, and Thread 2 reads that volatile variable and then reads the plain fields, the plain fields are guaranteed to be visible!
public class ConfigPublisher {
private int timeoutMs; // Plain field (NOT volatile)
private String endpoint; // Plain field (NOT volatile)
private volatile boolean ready; // Volatile publication gate
public void publish() {
this.timeoutMs = 5000; // (1) Plain write
this.endpoint = "https://api";// (2) Plain write
this.ready = true; // (3) Volatile write (StoreStore + StoreLoad barrier)
}
public void consume() {
if (this.ready) { // (4) Volatile read (LoadLoad + LoadStore barrier)
// By transitivity: (1) -> (2) -> (3) -> (4) -> (5)
// Guaranteed to see timeoutMs == 5000 and endpoint == "https://api"!
System.out.println(this.endpoint + " : " + this.timeoutMs); // (5)
}
}
}
JDK Internal Usage: AbstractQueuedSynchronizer (volatile int state) and LinkedBlockingQueue (AtomicInteger count) rely heavily on piggybacking so that reading/writing queue node pointers does not require volatile barriers on every node field.
Safe Publication & final Field Freeze Semantics (JLS §17.5)
Why are Java String and record classes thread-safe even when published across threads without volatile?
- When an object has
finalfields, the JVM inserts aStoreStorefreeze barrier at the end of the constructor before the object reference can be stored anywhere. - Crucial Rule: Any thread that sees a reference to that object (even via a non-volatile data race!) is guaranteed to see the initialized values of all its
finalfields (and any objects reachable through thosefinalfields), provided thethisreference did not escape during construction (e.g., registeringthisin a listener inside the constructor).
java.util.concurrent Memory Consistency Properties
Beyond core JLS rules, the java.util.concurrent package specification guarantees happens-before edges for higher-level utilities:
- Concurrent Collections (
BlockingQueue,ConcurrentHashMap): Placing an object into a concurrent collection happens-before another thread retrieves or removes that object from the collection. ExecutorService&CompletableFuture: Submitting aRunnable/Callablehappens-before its execution begins. Completing aCompletableFuturehappens-before any dependent stage (thenApply,thenAccept) or blocking consumer (get(),join()) returns.- The
LongAdder.sum()Exception (Gotcha!): UnlikeAtomicLong.get(),LongAdder.sum()iterates over stripedCell[]counters sequentially without acquiring a global lock or establishing a cross-cell happens-before snapshot. UseLongAdderfor high-throughput metrics/telemetry, never as a synchronization gate or sequence generator!
4. Race Conditions vs. Data Races & The 7 Production Patterns
Engineers frequently conflate Data Race and Race Condition. They are neither identical nor subsets of each other:
| Concept | Formal Definition | Example |
|---|---|---|
| Data Race | Two threads access the same memory location concurrently, at least one access is a write, and there is no Happens-Before edge ordering them. | Reading/writing a non-volatile stop flag across threads. |
| Race Condition | Program correctness depends on the relative timing or interleaving of threads violating an application invariant. | Checking if (stock > 0) then decrementing stock--, even if stock is a volatile variable or stored in a thread-safe map! |
Can you have a Data Race WITHOUT a Race Condition? (Benign Data Race)
Yes—String.hashCode() in the JDK:
public int hashCode() {
int h = hash; // Plain int field (NOT volatile!) — intentional Data Race!
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value) : StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h; // Plain write
}
}
return h;
}
Why is this safe in java.lang.String? Because it satisfies all 4 Benign Data Race preconditions:
- The field is a 32-bit
int(guaranteed atomic read/write by JLS §17.7, no word tearing). - The computation is idempotent and deterministic (every thread computes the exact same hash from the immutable
final byte[] value). - The value transitions at most once from
0toh. - No other mutable state piggybacks on
hashfor visibility.
Can you have a Race Condition WITHOUT a Data Race?
Yes! Every access below is protected by synchronized (via Vector or ConcurrentHashMap.get/put), so there is zero Data Race, yet the compound action still suffers a fatal Race Condition:
// Zero Data Race (every get/put is volatile/synchronized), 100% Race Condition!
if (!concurrentMap.containsKey(key)) {
concurrentMap.put(key, computeExpensiveValue()); // Another thread can slip in between!
}
// Fix: Use atomic compound action concurrentMap.computeIfAbsent(key, k -> ...)
The 5 Classic Race Condition Archetypes in Production
1. Read-Modify-Write (count++ Bytecode Interleaving)
Why does volatile int count; count++; lose updates when two threads increment it? Compiling and running javap -c reveals that count++ is four separate bytecode instructions:
0: getstatic #7 // Step 1: Read count from memory into operand stack (e.g., 41)
3: iconst_1 // Step 2: Push constant 1
4: iadd // Step 3: Add in ALU register (41 + 1 = 42)
5: putstatic #7 // Step 4: Write 42 back to memory
volatile only guarantees that Step 1 reads the latest value and Step 4 flushes immediately. If Thread A and Thread B both execute getstatic at count = 41, both compute 42 and write 42—one increment is permanently lost.
- Fix: Use
AtomicInteger.incrementAndGet()(hardware CAS loopLOCK CMPXCHG) orLongAdder(under high contention).
2. Check-Then-Act (Inventory Overselling)
Checking a condition and acting on it in two separate steps allows another thread to invalidate the condition in the window between Check and Act:
// WRONG: Check and Act are separate atomic operations
if (stock.get() > 0) {
stock.decrementAndGet(); // Can drop to -1 if two threads pass the check when stock == 1!
}
// CORRECT: Lock-free CAS loop combining Check + Act atomically
public boolean tryReserveStock(AtomicInteger stock, int quantity) {
while (true) {
int current = stock.get();
if (current < quantity) {
return false; // Out of stock
}
if (stock.compareAndSet(current, current - quantity)) {
return true; // Atomically verified current hasn't changed and updated!
}
// CAS failed due to contention; loop retries with fresh value
}
}
3. Multi-Variable Invariant Trap (AtomicInteger Atomic Transaction!)
Suppose a warehouse invariant requires available + reserved == 100. A developer uses two AtomicInteger fields:
private final AtomicInteger available = new AtomicInteger(100);
private final AtomicInteger reserved = new AtomicInteger(0);
public void reserveOne() {
available.decrementAndGet(); // Step A: available becomes 99
// <--- Window where an observer thread sees available(99) + reserved(0) == 99!
reserved.incrementAndGet(); // Step B: reserved becomes 1
}
Even though each field is atomic, atomicity does not compose across multiple independent variables. Furthermore, if you wrap reserveOne() in synchronized to protect writes, any reader thread checking available + reserved MUST also acquire that exact same lock—otherwise the reader will observe torn intermediate states (99 or 101) mid-mutation!
4. Monitor Target Pitfalls (synchronized on the Wrong Object)
- Instance vs. Static Lock Mismatch: Putting
synchronizedon an instance method locksthis, while astatic synchronizedmethod locksMyClass.class. Two threads calling those two methods do not block each other at all. - Locking on Value-Based / Boxed Primitives (
synchronized (count)): Becausecount++autoboxesInteger.valueOf(count + 1)into a brand-newIntegerinstance on every increment, threads lock on different objects! Even worse,Integer.valueOf(-128..127)shares cached JVM singletons, risking deadlocks with unrelated libraries (flagged by JEP 390 warnings).
5. Hidden Stateful Utilities in static final Singletons
Two standard JDK classes look like stateless utilities but hold mutable internal fields—making private static final declarations catastrophic in multi-threaded web servers:
SimpleDateFormat(calendar.setTime()corruption): InsideSimpleDateFormat.parse(), the parser mutates an internalCalendarBuilderand callscalendar.clear(). Under 8 concurrent threads running 80,000 parses,~80%fail—either throwingNumberFormatExceptionor silently returning corrupted dates (e.g., year2260instead of2026)!- Fix: Replace with immutable
java.time.format.DateTimeFormatter.
- Fix: Replace with immutable
java.util.regex.Matcher(groups[],from,tocorruption):Patternis immutable and thread-safe (static final Patternis great), butMatcherstores mutable cursor indices (first,last,groups[]). Sharing astatic final Matcheracross threads causes~25%of regex matches to silently return wrong substrings or false negatives!- Fix: Keep
static final Pattern, and instantiatepattern.matcher(input)per invocation.
- Fix: Keep
6. Database Lost Update (When JVM Locks Aren't Enough)
Synchronizing inside a Java pod (synchronized or ReentrantLock) only coordinates threads inside a single JVM heap. When 5 Kubernetes pods run SELECT stock FROM products WHERE id = 42 followed by UPDATE products SET stock = ?, JVM locks are useless.
- Fix: Push the atomic Check-Then-Act into the database engine using a conditional atomic update:
UPDATE productsSET stock = stock - :qtyWHERE id = :productId AND stock >= :qty;-- Check affected_rows == 1 in application code!
5. ThreadLocal Map Internals & Memory Leaks
ThreadLocal avoids synchronization by giving each thread its own isolated copy of a variable—useful for per-request context or legacy non-thread-safe objects when refactoring isn't possible.
Internal Architecture
- Every
java.lang.Threadinstance owns a fieldThreadLocal.ThreadLocalMap threadLocals. - Each
EntryinThreadLocalMapextendsWeakReference<ThreadLocal<?>>(the key is weak), while the value (Object v) is held via a strong reference:
🧬ThreadLocal Lifecycle: Active
Before GC: ThreadLocal Reference Active
WeakRef Key: Referenced (Active)
StrongRef Value: Reachable & Active
Leak Status: No Leak (Safe)
- Memory Operations:
- The thread holds a strong reference to the ThreadLocalMap.
- The map entry holds a WeakReference to the ThreadLocal key (e.g., UserContext).
- The value object (e.g., User entity) is strongly referenced and reachable.
💡 Switch between Before GC and After GC tabs to analyze how ThreadLocal reference leaks occur.
Why the Memory Leak Happens in Thread Pools
- In Tomcat or
ThreadPoolExecutor, worker threads live for the entire lifetime of the JVM. - If a
ThreadLocalinstance is garbage-collected (e.g., dynamic classloader reload or non-staticThreadLocal), the weak key becomesnull. - However, the strong reference chain
Thread -> ThreadLocalMap -> Entry -> valueremains unbroken until the thread either dies or callsremove()(or enough subsequentThreadLocaloperations trigger heuristic stale-entry expunging). - Worse still: if the
ThreadLocalisstatic final(the standard pattern), the key never becomesnull! Failing to callremove()means the value not only leaks memory, it leaks tenant/security context into the next HTTP request handled by that pooled thread!
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
public void handleRequest(UserContext ctx) {
CONTEXT.set(ctx);
try {
processBusinessLogic();
} finally {
CONTEXT.remove(); // MANDATORY: prevents both memory leak and cross-request tenant bleed!
}
}
Senior/Staff Interview Playbook: How to Ace JMM, Race Conditions & Happens-Before Interviews
When an interviewer asks "Why does a thread loop forever without volatile?" or "How do you fix a race condition on a counter?", a mid-level candidate talks vaguely about "CPU cache flushing." A Principal Engineer grounds the answer in C2 JIT compiler optimizations, Store Buffers, JLS §17.4.5 Happens-Before edges, and multi-node failure boundaries.
1. The 60-Second Senior Framing Strategy
- Separate Compiler from Hardware: Explain immediately that JMM bugs arise at two distinct layers: JIT Compiler optimizations (C2 loop-invariant hoisting, dead-store elimination) and CPU Hardware memory models (Store Buffers causing Store-Load reordering on x86-64 TSO, plus Load-Load/Store-Store reorderings on ARMv8/Graviton).
- State the Invariant via Happens-Before: Frame visibility and ordering strictly through Happens-Before edges () rather than "flushing RAM." Without a happens-before edge between conflicting accesses, the program has a Data Race.
- Distinguish Data Race vs. Race Condition: Point out that eliminating data races (e.g., making fields
volatileor usingConcurrentHashMap) does not eliminate race conditions if compound actions (Read-Modify-Write, Check-Then-Act, or multi-variable invariants) are not executed atomically. - Draw the Process Boundary: Always ask whether the invariant lives inside a single JVM (solved by CAS / locks) or spans multiple Kubernetes pods (requiring conditional SQL
UPDATE ... WHERE, optimisticversioncolumns, or distributed fencing tokens).
2. Classic Interviewer Traps & Principal Rebuttals
Trap 1: "When a thread loops forever on while (!stop), it's because the CPU core cached stop = false in its L1 cache, right?"
- Junior/Mid Answer: "Yes, each CPU core has its own L1 cache, and without
volatile, the CPU never syncs L1 cache with main RAM." - Principal Rebuttal: "Actually, hardware L1 caches on x86 and ARM are kept coherent automatically within nanoseconds by the MESI cache coherence protocol. The infinite loop is caused by the C2 JIT compiler performing loop-invariant hoisting: seeing no synchronization edge on
stopinside the loop, C2 hoists the read into a CPU register once before the loop (if (!reg) while(true)). You can prove this empirically by running with-Xintor-XX:TieredStopAtLevel=1—the loop terminates immediately withoutvolatilebecause MESI propagates the cache line!"
Trap 2: "If I put System.out.println() or Thread.sleep(0) inside while (!stop), the loop stops! Doesn't that fix the bug without volatile?"
- Junior/Mid Answer: "Yes,
printlnusessynchronizedinternally, which flushes all variables in memory." - Principal Rebuttal: "That is a dangerous Heisenbug. Under JLS §17.4.5, a lock acquisition inside
printlnonly establishes a happens-before edge with another thread that locks the exact same monitor. If the writer thread just setsstop = truewithout lockingSystem.out, there is zero formal happens-before edge onstop. It only works by accident today because opaque method calls and native syscalls inhibit C2 from hoistingstopacross the call boundary—an optimization behavior that can change with any JDK update or inlining heuristic."
Trap 3: "Why did our Double-Checked Locking (DCL) singleton work for 5 years on AWS EC2 Intel instances and immediately throw NullPointerException when we migrated to AWS Graviton (ARM64)?"
- Junior/Mid Answer: "ARM CPUs are faster so threads collide more often."
- Principal Rebuttal: "x86-64 uses Total Store Order (TSO), which prohibits hardware
Store-StoreandLoad-Loadreordering—only allowingStore-Loadreordering via the Store Buffer. Ininstance = new Service(), initializing the constructor fields and writing theinstancepointer are two stores (Store-Store). On x86, unless C2 reordered them, hardware kept the constructor stores ordered before the reference publication. ARMv8 (Graviton) uses a weakly-ordered memory model that freely reordersStore-StoreandLoad-Loadin hardware, allowing a reader core to see the non-nullinstancepointer before the constructor's field stores drain—unlessinstanceis declaredvolatile(which emitsstlr/ldarrelease-acquire barriers on ARM64) or initialized viafinalfields."
Trap 4: "To keep available + reserved == 100 thread-safe without locks, I changed both int fields to AtomicInteger. Is my class thread-safe now?"
- Junior/Mid Answer: "Yes,
AtomicIntegeruses CAS so neither variable can have lost updates." - Principal Rebuttal: "No—individual field atomicity does not compose into multi-variable transactional atomicity. Between
available.decrementAndGet()andreserved.incrementAndGet(), there is an observable window whereavailable + reserved == 99. Worse, even if you wrap the writer method insynchronized, any reader method that computesavailable.get() + reserved.get()without acquiring the exact same lock will still read torn intermediate states. To make a multi-variable invariant lock-free, you must pack both values into a single immutable value object (e.g.,record StockState(int available, int reserved)) held inside a singleAtomicReference<StockState>updated via CAS."
Trap 5: "Notice how ConcurrentHashMap is thread-safe. Why did my rate-limiter using map.compute(key, ...) freeze the entire application under load?"
- Junior/Mid Answer: "Because
ConcurrentHashMaplocks the whole map duringcompute()." - Principal Rebuttal: "In Java 8+,
ConcurrentHashMap.compute()acquires asynchronizedmonitor lock on the head node of that hash bucket. While that is fine for a 10-nanosecond counter increment, if a developer puts a blocking I/O call (like a Redis check or HTTP call) inside thecompute()lambda, the thread holds the bucket monitor across network I/O—blocking every other thread whose key hashes to that bucket and risking recursive lock deadlocks. Keepcompute()/merge()strictly CPU-bound and side-effect-free."
3. Mid-Level vs. Principal Candidate Cheat Sheet
| Concept / Scenario | Mid-Level Candidate Says | Principal Candidate Demonstrates |
|---|---|---|
Why while (!stop) hangs | "The CPU caches stop in L1 cache and never flushes to RAM." | "MESI keeps L1 coherent; C2 JIT loop-invariant hoisting lifts !stop out of the loop into a register (-Xint proves it)." |
Why x=1; r1=y vs y=1; r2=x yields 0, 0 | "Threads interleaved in a weird order." | "Even under sequential interleaving 0, 0 is impossible; it happens because writes sit in each core's Store Buffer (Store-Load reordering), requiring a StoreLoad fence." |
volatile Overhead & Guarantee | "volatile makes a variable thread-safe and skips CPU cache." | "volatile disables JIT hoisting, emits StoreStore/StoreLoad (write) and LoadLoad/LoadStore (read) barriers, and enables transitive piggybacking, but does not make count++ atomic." |
| Safe Publication of Immutable Objects | "Immutable objects never need synchronization." | "JLS §17.5 guarantees visibility of final fields via a constructor freeze barrier, only if this does not escape during construction." |
| Stateful Static Singletons | "SimpleDateFormat throws exceptions in threads." | "Sharing static final SimpleDateFormat or Matcher causes silent data corruption (~80% wrong dates / ~25% missed regex matches); use DateTimeFormatter and per-call pattern.matcher()." |
| Fixing Multi-Field Race Conditions | "Make both fields AtomicInteger." | "Combine coupled fields into an immutable record inside AtomicReference<State> with CAS, or guard both reads and writes with the same lock." |
