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?
answer
- null / stale / non-null-but-uninitialized
- data race ⇒ no happens-before ⇒ no visibility obligation
- reference never torn; contents unordered
- JIT reordering + weak hardware ordering
- final field with no this-escape is the exception
basics
~20 sThe 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.
solid answer
~60 sWith no synchronization on either side, the two threads are in a **data race** and the Java Memory Model constrains almost nothing. Three legal observations: 1. **null** — the write may not be visible at all, and may never become visible, since nothing forces progress. 2. **A stale value** — a previous reference the reader saw or cached. 3. **A non-null reference to a partially constructed object** — the reference is visible but the constructor's field writes are not, so the reader sees default values. The third is the surprising one. The JMM defines visibility through *happens-before*; without an edge between the writer's actions and the reader's read, the constructor's writes and the reference write are simply unordered with respect to the read, and the reader may observe any subset. Implementations make this concrete: the JIT may sink or reorder the initializing stores relative to the publishing store, and weakly ordered hardware may make them globally visible out of order. Crucially the object is not "half-copied" — the reference itself is never torn. The reference is fine; the *ordering* of its contents is not.
code
java · 17 linesclass Holder {
int n;
Holder() { n = 42; }
}
class Publisher {
Holder holder; // plain field
void publish() { holder = new Holder(); } // thread A
void use() { // thread B
Holder h = holder;
if (h != null && h.n != 42) {
throw new AssertionError("saw a partially constructed object");
}
}
}go deeper
State the three observable outcomes — null, stale, or non-null with default field values — and that the fix is establishing a happens-before edge.
Explain why the specification allows it: a data race leaves the constructor's writes unordered relative to the reader's access, and name both the JIT and hardware sources of reordering.
Add the precision — references are never torn, final fields with no this-escape are exempt — and explain why the defect survives testing and surfaces under production load on weakly ordered hardware.
Frame publication correctness as something established by argument about happens-before, not by testing, and make it a reviewable property of the design rather than an incident to be diagnosed later.
## The scenario ```java class Holder { int n; String s; Holder() { n = 42; s = "ready"; } } class Publisher { Holder holder; // plain field: not final, not volatile void publish() { holder = new Holder(); } // thread A void use() { Holder h = holder; // thread B if (h != null) { assert h.n == 42; } } // may fail } ``` The assertion can fail. This is not a JVM bug; it is exactly what the Java Memory Model licenses. ## What the specification actually says The JMM defines when a read is permitted to see a write. When two accesses to the same location are performed by different threads, at least one is a write, and they are not ordered by *happens-before*, the execution contains a **data race**. In a racy execution, the read is not obliged to see the most recent write in real time — the notion of "most recent" is not even well defined across threads. Here there are two independent races: on `holder` itself, and on `holder.n` / `holder.s`. Nothing in the program creates a happens-before edge between thread A's actions and thread B's read. So the specification allows thread B's read of `holder` to return the new reference while its subsequent reads of `n` and `s` return the default values written when the object's memory was allocated and zeroed. That combination — reference visible, contents not — is what makes unsafe publication uniquely nasty. The reader's null check passes. The object exists. Its invariants are simply not there yet. ## Why implementations really do this The rule is not theoretical pedantry; two independent layers produce it. **The compiler.** Source-level `holder = new Holder()` becomes, roughly: allocate memory, run the constructor's stores, then store the reference into the field. Nothing in the Java Memory Model forces that order to be preserved *as seen by another thread*, so the JIT is entitled to schedule the publishing store before some initializing stores, or to keep initializing values in registers and write them later. From the writing thread's own point of view the result is indistinguishable, which is the only constraint an optimizer must respect for racy data. **The hardware.** Even with the compiler's order preserved, store visibility order is an architecture property. On a total-store-order architecture such as x86-64, stores become visible in program order, so this particular reordering does not occur at the hardware level. On weakly ordered architectures — AArch64, POWER — stores may become visible out of order unless a barrier forbids it. The same class file therefore behaves differently across machines, which is precisely why the language defines a memory model instead of deferring to the hardware. ## What is *not* a legal outcome Precision matters here, because candidates over-claim: - **The reference is never torn.** A reference read yields either the old value or the new one, never a mixture. Reference (and 32-bit) reads and writes are atomic in the sense of never producing an invented bit pattern. - **The object is never a mixture of two different objects.** "Partially constructed" means *this* object with some fields not yet visible, not fragments of several objects. - **A `final` field is different.** Fields declared `final` carry a separate guarantee: provided the constructor does not let `this` escape, a thread that reads the reference is guaranteed to see the correctly initialized final field, even through a racy publication. That is a distinct spec rule, and it is exactly why immutable objects are safe to hand around casually. ## Why testing almost never catches it Three factors conspire. First, on x86-class hardware the store-ordering half of the problem does not manifest, so the window is narrow. Second, the JIT only applies the aggressive reorderings after a method is hot, so short tests run interpreted or C1-compiled code that behaves benignly. Third, the window is nanoseconds wide and requires two threads to interleave precisely. So the bug surfaces late: under production load, after warm-up, on a many-core or AArch64 machine — and it appears as a `NullPointerException` on a field that "cannot" be null, or an object whose invariants are violated in a way the code cannot produce. The correct engineering conclusion is that publication correctness must be established by reasoning about happens-before, not by testing. ## The general shape of the fix Every fix does the same thing: create a happens-before edge such that the constructor's writes are ordered before the reader's access to the object. Whether that edge comes from a monitor, a volatile write and read, class initialization, the final-field freeze, or a thread-safe container's internal ordering is a design choice; the requirement is identical in all cases.
- Can the reader see a torn reference — half of the old address and half of the new one?No. Reads and writes of references are atomic in the Java Memory Model: the reader sees either the old value or the new one, never an invented intermediate. The problem is not the reference's integrity but the ordering of the object's field writes relative to the publishing write.
- If the same code is only ever run on x86-64 servers, is unsafe publication still a real risk?Yes. x86-64's total store ordering removes the hardware half of the reordering, but the JIT compiler is still free to reorder or sink the initializing stores relative to the publishing store once the method is hot, and it does so on any architecture. Relying on a particular processor's memory ordering also breaks the moment the service is deployed on AArch64 hardware.
- Does adding a null check on the reader side make the publication safe?No. The dangerous case is precisely the one where the reference is non-null — the check passes and the object's fields are still at their defaults. A null check narrows nothing about visibility ordering; only a happens-before edge does.
Publishing unsafely is mailing someone your new address before the furniture arrives. The address is valid, they show up, and the house is empty — nothing is broken, the notification just outran the contents.
saying these in an interview costs you the question
- Claiming the reader can only ever see null or the fully constructed object.
- Saying the reference can be 'torn' or half-written.
- Believing a null check on the reading side makes publication safe.
- Assuming that because it never reproduces on a developer laptop, the code is correct.
- Thinking the constructor 'finishes' before the assignment in a way other threads are guaranteed to observe.