skip to content

Memory Model

The contract that defines when one thread's writes become visible to another, which reorderings are permitted, and what is atomic. Interviewers treat it as the deep end of concurrency, because correct multithreaded code has to rest on these rules rather than on whichever hardware you happened to test.

on this pageshow

explore

questions

20

Which individual field and array-element reads and writes does the Java Language Specification guarantee to be atomic, and which ones does it explicitly leave non-atomic?

level: middleimportance: must knowfreq 50%

answer

  1. Refs atomic; all primitives except long/double
  2. long/double may split into two 32-bit halves
  3. volatile long/double always atomic
  4. Atomic ≠ visible, atomic ≠ ordered
  5. One access atomic ≠ x++ atomic

basics

~20 s

Reads and writes are atomic for object references and for every primitive type except long and double. Non-volatile long and double accesses may be split into two 32-bit halves, so a reader can see a mix of two values. Declaring them volatile makes them atomic again.

solid answer

~60 s

The rule lives in the Java Language Specification's section on non-atomic treatment of `double` and `long` (§17.7): - **Atomic:** reads and writes of *references* and of all primitives **except** `long` and `double`. "Atomic" here means indivisible — a read returns some value that some write actually stored, never a blend of two. - **Not guaranteed atomic:** a single non-volatile `long` or `double` read or write, which an implementation may treat as two separate 32-bit actions. A concurrent reader can then observe the high half of one write with the low half of another — a value that was never written by anybody. - **Atomic again if `volatile`:** the specification requires `volatile long` and `volatile double` accesses to be atomic on every implementation. The same applies to array elements: `long[]` and `double[]` elements carry the same caveat. Two boundaries matter. Atomicity is only about one access being indivisible — it says nothing about whether another thread sees it promptly. And it says nothing about compound operations: `x++` is several accesses, each atomic, and still racy.

code

java · 8 lines
java
class Holder {
    Object ref;          // read/write atomic
    int    counter;      // read/write atomic
    double ratio;        // MAY tear: two 32-bit halves allowed
    long   position;     // MAY tear
    volatile long safePos; // atomic by specification, on every platform
    long[] samples;      // each element: same caveat as a long field
}

go deeper

for a junior

Recall the list: references and all primitives except long and double have atomic reads and writes, and volatile fixes those two.

for a middle

State the rule and immediately add the two boundaries — atomicity is not visibility, and one atomic access is not an atomic compound operation.

for a senior

Explain the design rationale (cost on 32-bit platforms, opt-in via volatile), extend it to array elements, and point to AtomicLongArray/VarHandle where element-level atomicity is needed.

for a principal

Frame it as the specification drawing a portability floor rather than describing any one machine, and insist that code be written to the specification rather than to observed 64-bit behaviour.

## What "atomic" means here, precisely In this part of the Java Language Specification, an access being **atomic** means it is *indivisible*: a read observes exactly the bit pattern written by one write, and a write is observed either entirely or not at all. It does not mean "immediately visible", "ordered", or "safe to combine with other operations". Those are separate guarantees with separate rules. ## The guarantee, item by item The specification's rule (JLS §17.7, unchanged in substance for many releases) is: 1. **Reference reads and writes are atomic**, regardless of whether references are 32 or 64 bits wide in a given implementation. A reader sees either the old reference or the new one, never a mixture of their bits. This is essential — a torn reference would be a wild pointer, and the entire memory-safety story of the platform would collapse. 2. **Reads and writes of primitives are atomic, with the explicit exception of `long` and `double`.** So `boolean`, `byte`, `short`, `char`, `int` and `float` accesses are indivisible. 3. **A single non-volatile `long` or `double` read or write may be treated as two separate 32-bit reads or writes.** This is the tearing licence. If one thread writes `0x0000_0000_FFFF_FFFF` over a field previously holding `0x7FFF_FFFF_0000_0000`, a concurrent reader may legally observe `0x7FFF_FFFF_FFFF_FFFF` — a value never written by any thread. 4. **`volatile long` and `volatile double` accesses are always atomic.** Implementations must guarantee this even on platforms whose natural word is 32 bits. Array elements follow the element type: elements of a `long[]` or `double[]` carry the same caveat, elements of an `int[]` do not. ## Why the exception exists at all The specification was written to be implementable on 32-bit machines, where a 64-bit store has no single native instruction guaranteed to be atomic. Rather than force every implementation to pay for a locked or wide instruction on every `long` field access — including in the overwhelmingly common single-threaded case — the language made plain 64-bit accesses cheap and required atomicity only where the programmer asks for it with `volatile`. Making `volatile` the opt-in keeps the cost aligned with the intent. ## What the guarantee does *not* buy you This is where most interview answers go wrong, so be explicit about two boundaries. **Atomicity is not visibility or ordering.** An `int` write is atomic, but another thread may go on reading a stale value indefinitely, and surrounding operations may be observed out of order. Those questions belong to the visibility and ordering parts of the memory model, and are exactly what `volatile`, locks and other synchronisation provide beyond atomicity. A common mistake is to reason "my field is an `int`, so writes are atomic, so no synchronisation is needed" — atomic-but-invisible is a perfectly ordinary outcome of a data race. **Atomicity of each access is not atomicity of a sequence.** `count++` is a read, an add, and a write. Each part is atomic; the combination is not, so two threads can lose an update. Likewise, check-then-act sequences such as "if absent, put" are three or four atomic accesses forming one non-atomic operation. Individual-access atomicity is a *floor*, not a concurrency strategy. ## Practical guidance - If a `long` or `double` field is shared across threads without other synchronisation, mark it `volatile` (or route access through an `AtomicLong` / `VarHandle`). This is correctness by specification, not superstition — do not reason from what your current 64-bit hardware happens to do. - If a field of any type is shared and you need the reader to see updates, you need more than atomicity; you need synchronisation. - If you are combining a read and a write, atomicity of each half is irrelevant. Use a lock, an atomic class, or a `VarHandle` compound operation. - Fields protected by a lock, or confined to one thread, need none of this — atomicity concerns only concurrent unsynchronised access. ## Being precise in the answer A strong answer states the rule as a list — references atomic, primitives atomic except `long`/`double`, `volatile` restores atomicity for those two — and then adds the two disclaimers about visibility and compound actions without being asked. That combination shows you know both what the specification promises and what people wrongly infer from it.

  • A shared `int` field is written by one thread and read by another with no synchronisation. The write is atomic — so is the code correct?
    No. Atomicity only guarantees the reader never sees a half-written value; it says nothing about when, or whether, the reader observes the new value at all. Without synchronisation the reader may keep returning a stale value indefinitely, because nothing establishes the ordering and visibility relationship between the two threads. You need `volatile`, a lock, or another synchronisation mechanism.
  • Does the atomicity rule apply to elements of a `long[]`?
    Yes — an array element behaves like a field of its component type, so `long[]` and `double[]` elements may be treated non-atomically while `int[]` elements are atomic. There is no way to declare an individual array element `volatile`, so for atomic element access you use `AtomicLongArray` or a `VarHandle` produced by `MethodHandles.arrayElementVarHandle`.
  • Why did the language make plain 64-bit accesses non-atomic instead of requiring atomicity everywhere?
    Because on 32-bit platforms a 64-bit store has no cheap guaranteed-atomic instruction, so requiring it would tax every `long` and `double` access — including the vast majority that are never shared — to serve a rare case. Making `volatile` the opt-in puts the cost exactly where the programmer signals that concurrent access matters.

saying these in an interview costs you the question

  • Saying all Java field accesses are atomic without the long/double exception
  • Claiming reference writes can tear on 64-bit platforms
  • Believing atomic writes remove the need for synchronisation (confusing atomicity with visibility)
  • Thinking `volatile` makes `x++` thread-safe
  • Assuming the long/double caveat applies to fields only and not to array elements

context

open as a page

In the Java Memory Model, what special visibility guarantee applies to an instance field declared `final`, and what must the reading thread do to obtain it?

level: middleimportance: must knowfreq 55%

basics

~20 s

Final fields are frozen when the constructor returns. Any thread that later obtains the object's reference sees those fields correctly initialized, with no lock and no volatile on the reader's side, provided the reference did not escape during construction.

open as a page

Which specific rules does the Java Language Specification list as creating a happens-before edge between two actions? Enumerate them.

level: middleimportance: must knowfreq 65%

basics

~20 s

Program order within a thread; unlocking a monitor before any later lock of it; a volatile write before any later read of that field; Thread.start before the new thread's actions; a thread's actions before a successful join; interrupt before the interrupt is detected; constructor end before the finalizer; default writes before any thread starts; plus transitivity.

open as a page

In Java, the `volatile` keyword, `synchronized` blocks, `final` fields, and the `java.util.concurrent.atomic` classes (or `VarHandle` access modes) each grant a *different subset* of the guarantees the Java Memory Model specifies. For each of those four constructs, state precisely what the specification grants and what it withholds, and give a concrete bug that follows from picking the wrong one.

level: middleimportance: must knowfreq 62%

basics

~20 s

volatile: visibility and ordering for that field plus single read/write atomicity, but no compound atomicity. synchronized: all three, only for threads taking the same lock. final: a one-time publication freeze at constructor end, nothing else. Atomics/VarHandle: single-variable atomicity, never multi-variable.

open as a page

Why does the Java Language Specification define a memory model at all? What would be left unspecified about a multi-threaded Java program if it did not?

level: middleimportance: must knowfreq 55%

basics

~20 s

Because compilers and CPUs freely transform code, the language must state which value a read of a shared field may legally return. The memory model is that contract: it bounds optimization and reordering so one program behaves the same on every CPU.

open as a page

One thread assigns a newly constructed object to an ordinary non-final, non-volatile field, and another thread reads that field without any synchronization. Under the Java Memory Model, what states can the reading thread legally observe, and why does the specification permit them?

level: middleimportance: must knowfreq 58%

basics

~20 s

The reader may see null, a stale earlier value, or a non-null reference to an object whose fields still hold default values (0/false/null). The Java Memory Model permits this because a data race with no happens-before edge leaves the constructor's writes unordered relative to the reference read, so any of them may not yet be visible.

open as a page

A class assigns all of its fields in its constructor and the fields are declared `final`, yet another thread occasionally observes those fields at zero or null. Explain how that is possible and what in the constructor causes it.

level: seniorimportance: must knowfreq 45%

basics

~20 s

The constructor let this escape — it published the object (started a thread, registered a callback, stored itself somewhere) before the constructor finished. The frozen-field guarantee only covers references obtained after construction completes, so an early reference sees defaults.

open as a page

If one thread assigns an object reference to a field while another thread reads that field, can the reader ever observe a half-written, invalid reference — for instance on a 64-bit JVM where a pointer is eight bytes wide?

level: juniorimportance: should knowfreq 35%

basics

~20 s

No. The Java Language Specification guarantees that reference reads and writes are atomic regardless of pointer width, so the reader sees either the old reference or the new one, never a mixture. It does not guarantee the reader sees the new one promptly, or sees the object fully initialised.

open as a page

A field is declared `final` and holds a reference to a `HashMap` that is later modified by several threads. What exactly does the memory model guarantee here, and what does it not?

level: middleimportance: should knowfreq 42%

basics

~20 s

It guarantees the reference itself, and the map contents as of the end of the constructor, are visible to any thread that obtains the object. It guarantees nothing about entries added or changed afterwards — those are unsynchronized races.

open as a page

A thread writes several plain (non-volatile) fields, then starts a worker thread that reads them; later the parent calls `join()` and reads fields the worker wrote. Which of these reads are guaranteed correct, and by which rule?

level: middleimportance: should knowfreq 45%

basics

~20 s

Both are guaranteed. Thread.start() happens-before every action in the started thread, so the worker sees everything written before the start. All actions in a thread happen-before a successful return from join() on it, so the parent sees everything the worker wrote.

open as a page

On a 64-bit HotSpot JVM running on x86-64, is a plain non-volatile `long` field ever actually torn? Explain the gap between what the hardware does and what the Java Language Specification permits, and when that gap still matters.

level: seniorimportance: should knowfreq 32%

basics

~20 s

In practice no: 64-bit HotSpot on x86-64 emits one naturally aligned 8-byte load or store, which the hardware performs atomically. The specification still permits tearing, so relying on the hardware is unportable, and you usually need volatile for visibility and ordering anyway.

open as a page

JSR-133 replaced Java's original memory model in Java 5. What was broken in the pre-Java-5 model, and what did the revision guarantee that the old one did not?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The old model was unimplementable and too weak: immutable objects with final fields could be seen partly built, volatile writes did not order surrounding normal writes, and double-checked locking was broken. JSR-133 added final-field freeze semantics and volatile ordering of nearby accesses.

open as a page

The Java Memory Model promises that correctly synchronized programs behave as if sequentially consistent. What does the specification mean by a correctly synchronized program, and what is still guaranteed for a program that is not?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Correctly synchronized (data-race-free) means every sequentially consistent execution has no data race: no two conflicting accesses unordered by happens-before. Such programs behave as a simple interleaving. Racy programs get weak guarantees only - but never values out of thin air.

open as a page

Inside HotSpot, the C2 JIT compiler is allowed to move, merge and delete field accesses in Java code. Name the C2 optimizations that actually remove or relocate memory operations, explain what the Java Memory Model lets C2 assume about a plain (non-volatile) shared field between two synchronization actions, and explain why the same source code can behave differently once tiered compilation promotes it from the interpreter to C1 and then to C2.

level: seniorimportance: should knowfreq 38%

basics

~20 s

C2 hoists loop-invariant loads into registers, eliminates redundant loads, sinks or deletes stores, and via escape analysis scalar-replaces objects so their fields have no memory operations at all. The Java Memory Model lets it treat plain fields as thread-local between synchronization actions. Tiered compilation applies this only once code is hot, so racy code changes behaviour after warm-up.

open as a page

Where does HotSpot actually place memory barriers around a read and a write of a volatile field, and how does the machine code it emits differ between x86-64 and AArch64?

level: seniorimportance: should knowfreq 26%

basics

~20 s

Conceptually: LoadLoad+LoadStore after a volatile read; StoreStore+LoadStore before a volatile write and StoreLoad after it. On x86-64 only the trailing StoreLoad costs an instruction, usually a locked stack operation. On AArch64 HotSpot uses ldar/stlr acquire/release instructions instead.

open as a page

The Java Language Specification has a section titled "Word Tearing" (§17.6) whose guarantee is different from its rule that `long` and `double` may be treated non-atomically. State the §17.6 guarantee and explain what it forces a JVM implementation to do on hardware that cannot store a single byte directly.

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

§17.6 forbids word tearing: updating one field or one array element must never disturb any other field or element. On hardware without byte-granularity stores, an implementation may not implement a byte[] element write as read-modify-write of the enclosing word — it must use an atomic operation instead.

open as a page

What does a JVM actually have to emit at the end of a constructor to deliver the frozen-field guarantee, and why do reading threads need nothing on their side?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

The JVM must keep the final-field stores from moving after the object reference becomes visible. HotSpot emits a store-store barrier at constructor end — free on x86's strongly ordered stores, a real instruction on ARM. Readers need nothing because address dependencies preserve the load order.

open as a page

A Java field declared volatile is read many times inside a hot loop and written only rarely. Break down what that ordered access actually costs the running JVM, and say which part of the cost usually dominates in a real profile.

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Three separate costs: the fence instructions emitted (architecture-dependent), the JIT optimizations the ordered read forbids, and cache-coherence traffic on the field's line. For read-mostly fields the blocked JIT work usually dominates, and hoisting one read into a local recovers most of it.

open as a page

The Java Memory Model defines correctness through a partial order of happens-before edges rather than by listing which reorderings a compiler or CPU may perform. What does that design choice buy, and what does it cost the people writing Java?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

A partial order is portable and implementation-neutral: it constrains observable outcomes, not instruction schedules, so every JVM, JIT and CPU can optimize freely while programs stay correct everywhere. The cost is that correctness becomes non-observable — races are invisible in testing and must be reasoned about.

open as a page

The Java Memory Model explicitly forbids out-of-thin-air values. What is an out-of-thin-air value, why must a language memory model rule them out, and why is that the hardest part of such a specification to write?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

An out-of-thin-air value is one a read returns that no write ever justified except circularly - the read causes the write that supplies it. Java forbids them so racy code stays type-safe; it is hard because the rule must block circular causality without banning real optimizations.

open as a page