Visibility vs atomicity, what volatile guarantees and what it does not, when volatile is insufficient, happens-before, why double-checked locking needs volatile, instruction reordering and CPU caches, memory barriers/fences and how Java implements them (volatile, VarHandle fences), false sharing and @Contended, VarHandle, AtomicStampedReference, limits of atomics, ThreadLocal internals, ThreadLocal memory leaks and ScopedValue.
Published September 25, 2026
JMM questions separate "knows the keywords" from "understands the hardware and the spec". Anchor every answer in happens-before:
volatile, locks, thread start and join, final fields and concurrent collections are the ways to create those edges.Short answer:
count++ is three steps (read, add, write), so two threads can interleave, and lose an update, even with perfect visibility.volatile gives visibility and ordering, but not compound atomicity. Atomics give atomic single-variable updates. Locks give both, for whole blocks.
private boolean running = true; // BUG: without volatile, this loop may never see the update
void stop() { running = false; }
void loop() { while (running) { work(); } }
Learn it in depth → volatile and the Memory Model
volatile guarantee, and what doesn't it guarantee?Short answer:
long/double (which aren't guaranteed atomic otherwise).x++, check-then-set);volatile enough, so you need synchronisation?Short answer: Whenever correctness depends on more than a single independent write:
count++, balance -= x): use an atomic or a lock;if (!initialised) init();): two threads can both see false;min <= max, or a pair of balance and version);volatile is right for flags, status fields written by one thread, and publishing immutable objects.
volatile variable be atomic? Give examples where they aren't.Short answer: A single read or write of a volatile is atomic (including long/double). Compound operations aren't:
volatile int hits;
hits++; // NOT atomic: read → +1 → write; concurrent increments are lost
volatile long max;
if (value > max) max = value; // NOT atomic: check-then-act race
volatile List<String> names = new ArrayList<>();
names.add("x"); // NOT safe: the reference is volatile, but the ArrayList isn't thread-safe
The fixes: AtomicInteger.incrementAndGet()/LongAdder, AtomicLong.accumulateAndGet(value, Math::max), and immutable lists replaced through an AtomicReference, or concurrent collections.
Short answer: A partial ordering guaranteeing that if action A happens-before action B, then A's effects are visible to B, and they appear ordered. The key rules:
t.start() happens-before any action in t.t happen-before another thread returns from t.join(), or sees t.isAlive() == false.interrupt() happens-before the interrupted thread detects it.final fields through a properly published reference.java.util.concurrent builds on these rules: a put into a ConcurrentHashMap or BlockingQueue happens-before the get/take that retrieves the element, and task submission happens-before execution.
A data race is two conflicting accesses (at least one of them a write) not ordered by happens-before. Programs free of data races are sequentially consistent (the DRF-SC guarantee).
volatile?Short answer: instance = new Singleton() isn't one step. It's: allocate → run the constructor → publish the reference. Without volatile, the compiler or CPU can reorder the publication before the constructor finishes. A second thread doing the unsynchronised first check then sees a non-null but partially constructed object, and uses fields that haven't been initialised. volatile (Java 5+ semantics) forbids that reordering: the volatile write comes after the constructor's writes, and the reader's volatile read happens-after it.
public final class Config {
private static volatile Config instance; // volatile is essential
public static Config get() {
Config local = instance; // one volatile read on the fast path
if (local == null) {
synchronized (Config.class) {
local = instance;
if (local == null) instance = local = load();
}
}
return local;
}
}
Usually, prefer the holder idiom, or an enum.
Short answer:
The JMM lets you write portable code: use volatile, locks or atomics, and the JVM inserts whatever barriers each platform needs.
Short answer: A memory barrier is an instruction that restricts reordering of memory operations across it, and forces visibility (draining store buffers, invalidating stale data). The classic kinds are LoadLoad, LoadStore, StoreStore and StoreLoad. StoreLoad is the most expensive: mfence or a lock-prefixed instruction on x86.
In Java:
volatile accesses (release semantics on a write, acquire semantics on a read, and a StoreLoad after volatile writes on x86), for monitor enter and exit, final field freezes, and atomic operations;VarHandle: VarHandle.fullFence(), acquireFence(), releaseFence(), loadLoadFence(), storeStoreFence() (Java 9). Previously, Unsafe.*Fence;VarHandle access modes give finer control: getAcquire/setRelease, getOpaque/setOpaque, getVolatile/setVolatile.Why it matters: fences are the mechanism behind every visibility guarantee. They also have a cost, which is why lock-free algorithms use the weakest ordering that's still correct.
Short answer: CPUs cache memory in 64-byte cache lines. If two threads write different variables that share a cache line, each write invalidates the line in the other core's cache (the MESI protocol). The line ping-pongs between cores, and performance collapses, even though the threads don't share data logically. It typically hits per-thread counters in an array, or adjacent fields updated by different threads.
Fixes:
@jdk.internal.vm.annotation.Contended: used internally by LongAdder's Cell, ConcurrentHashMap's counter cells, and Thread's fields. For application code, it needs --add-exports and -XX:-RestrictContended.Detect it with perf c2c, or by benchmarking with and without padding.
VarHandle?Short answer: A typed, strongly controlled reference to a variable (Java 9, java.lang.invoke): an instance field, a static field, an array element, or a memory segment. It offers:
compareAndSet, compareAndExchange, getAndAdd, getAndBitwiseOr, weak CAS;It's the supported replacement for sun.misc.Unsafe field access and CAS, and java.util.concurrent is built on it. It avoids the per-object overhead of AtomicLong wrappers when you need atomics on many objects' fields.
class Node {
volatile Node next;
private static final VarHandle NEXT;
static {
try { NEXT = MethodHandles.lookup().findVarHandle(Node.class, "next", Node.class); }
catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
}
boolean casNext(Node expected, Node update) { return NEXT.compareAndSet(this, expected, update); }
}
AtomicStampedReference work?Short answer: It holds a (reference, int stamp) pair, updated atomically together. compareAndSet(expectedRef, newRef, expectedStamp, newStamp) succeeds only if both the reference and the stamp match. Incrementing the stamp on every update means an A → B → A change is detected (the stamp moved from 1 to 3), which solves the ABA problem in lock-free structures and object pools. Internally, it CASes an immutable Pair object, so each update allocates. AtomicMarkableReference is the boolean-mark variant, used for logical deletion flags.
Atomic* classes?Short answer:
LongAdder).VarHandle or field updaters).ThreadLocal, and how does it work behind the scenes?Short answer: A ThreadLocal<T> gives each thread its own independent copy of a value. It's used for per-thread context (security context, transaction and MDC in logging frameworks, request IDs) and for non-thread-safe helpers (formerly SimpleDateFormat).
The internals:
Thread object has a ThreadLocalMap field.ThreadLocal instances, and its values are strong references.get()/set() look up the current thread's map, using the ThreadLocal's precomputed hash (an open-addressing table with linear probing).InheritableThreadLocal copies values into child threads when they're created, not into pool threads that are reused.private static final ThreadLocal<DecimalFormat> FMT = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
String format(BigDecimal v) { return FMT.get().format(v); }
ThreadLocal prevent memory leaks, or cause them? What is a ThreadLocal memory leak?Short answer: It doesn't prevent leaks. It's a common cause of them:
remove()d. That means memory growth, and data leaking between requests (a security bug: user A's context seen by user B).ThreadLocal object itself becomes unreachable, the key is cleared, but the value stays strongly referenced in the entry. It's only cleaned opportunistically, on later map operations.The rules:
set in try, and remove() in finally (filters, interceptors);ThreadLocals static final;Prefer ScopedValue (final in Java 25, JEP 506): immutable, bounded to a scope, inherited by structured-concurrency subtasks, with no remove() to forget.
static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();
ScopedValue.where(CONTEXT, new RequestContext(userId, traceId))
.run(() -> orderService.placeOrder(cmd)); // CONTEXT.get() is available inside; automatically unbound afterwards
Q: What is safe publication?
A: Making an object and its state visible to other threads through a happens-before edge: storing it in a volatile or final field, an AtomicReference, a concurrent collection, or under a lock, or initialising it in a static initialiser. Unsafe publication (a plain field) can expose partially constructed objects.
Q: Are final fields always visible to other threads?
A: Yes, if the object is properly constructed (this doesn't escape during construction). Other threads then see the final fields' values (and the objects reachable through them, as of construction) without synchronisation. That's the basis of thread-safe immutability.
Q: What's the difference between getOpaque, getAcquire and getVolatile in VarHandle?
A: Opaque guarantees the access isn't optimised away, and is coherent for that variable, with no ordering of other accesses. Acquire also prevents later reads and writes moving before it (paired with release writes). Volatile adds full sequential consistency with the other volatile accesses. Use the weakest mode that's still correct.
Q: How do you test concurrency correctness?
A: With jcstress (the OpenJDK harness that explores reorderings on real hardware), Lincheck (linearizability checking), stress tests with many threads plus assertions, and running on weakly ordered hardware (ARM) in CI. Use static analysis for @GuardedBy violations.