final fields get a special JMM guarantee: if an object is constructed correctly, any thread that reads a reference to that object is guaranteed to see the fully initialized values of its final fields — without any synchronization on the reading side. This is the foundation of safely publishing immutable objects.
The guarantee
The JMM inserts a freeze at the end of a constructor: all final field writes (and anything reachable through them) are guaranteed to be complete and visible before the constructor returns. A reader that obtains the reference after construction sees those final values, even via a data race on the reference itself. For non-final fields you get no such promise — they can appear stale or default.
final class Point {
private final int x;
private final int y;
Point(int x, int y) { this.x = x; this.y = y; }
int x() { return x; }
int y() { return y; }
}
class Box {
Point p; // NOT volatile, NOT final
void set() { p = new Point(1, 2); } // publish via a plain field
}
Even though Box.p is a plain field with a benign race, any thread that sees a non-null p is guaranteed to see x == 1 and y == 2, never 0. That works only because x and y are final.
The one condition: no this escape
The guarantee holds only if this does not escape during construction. If the constructor publishes the partly built object (registers a listener, starts a thread, stores this in a static), another thread can grab the reference before the freeze and see uninitialized finals.
class Leaky {
final int v;
Leaky(Registry r) {
r.register(this); // BAD: this escapes before v is frozen
v = 42;
}
}
Why it matters
This is what makes immutable objects (String, boxed primitives, records with final components) inherently thread-safe and freely shareable: no locks, no volatile, no defensive synchronization on readers.