Why Java’s final Fields Matter: Memory Model Rules Explained
This article explores Java’s memory model rules for final fields, detailing how writes and reads are ordered, the impact on object construction, examples with primitive and reference types, and processor-specific implementations, illustrating why final provides safe initialization without explicit synchronization.
Understanding Java final fields in the Java Memory Model
Java language keywords are carefully designed; this article dives into the JMM rules for final fields.
Compared with lock and volatile, reads and writes of final fields behave like ordinary variable accesses, but the compiler and processor must obey two reordering rules:
Writing a final field inside a constructor and then assigning the constructed object’s reference to a variable cannot be reordered.
The first read of an object reference and the subsequent first read of its final field cannot be reordered.
public class FinalExample {
int i; // ordinary variable
final int j; // final variable
static FinalExample obj;
public FinalExample() {
i = 1; // write ordinary field
j = 2; // write final field
}
public static void writer() {
obj = new FinalExample(); // thread A writes
}
public static void reader() {
FinalExample object = obj; // thread B reads reference
int a = object.i; // read ordinary field
int b = object.j; // read final field
}
}Assume thread A executes writer() and then thread B executes reader(). The following sections illustrate the two rules with this example.
Write‑final‑field reordering rule
The rule prevents the write to a final field from being reordered outside the constructor. It is enforced in two ways:
The JMM forbids the compiler from moving a final‑field write outside the constructor.
The compiler inserts a StoreStore barrier after the final‑field write and before the constructor returns, preventing the processor from reordering the write.
In the diagram below, the write to the ordinary field i is reordered outside the constructor, causing thread B to read an uninitialized value, while the write to the final field j stays within the constructor, so thread B reads the correctly initialized value.
Read‑final‑field reordering rule
The rule ensures that the first read of an object reference and the first read of its final field are not reordered by the processor. The compiler inserts a LoadLoad barrier before the final‑field read.
In the diagram below, the read of the ordinary field is reordered before the reference read, leading to a stale value, while the read of the final field occurs after the reference read, guaranteeing a correctly initialized value.
# If a final field is a reference type
When the final field holds a reference, the write rule adds an extra constraint: the write to a member of the referenced object inside the constructor cannot be reordered with the assignment of the object’s reference to a variable.
public class FinalReferenceExample {
final int[] intArray; // final reference
static FinalReferenceExample obj;
public FinalReferenceExample() {
intArray = new int[1]; // 1
intArray[0] = 1; // 2
}
public static void writerOne() { // thread A
obj = new FinalReferenceExample(); // 3
}
public static void writerTwo() { // thread B
obj.intArray[0] = 2; // 4
}
public static void reader() { // thread C
if (obj != null) {
int temp1 = obj.intArray[0]; // 6
}
}
}The JMM guarantees that thread C will at least see the write performed in the constructor (value 1). The write performed by thread B (value 2) may or may not be visible because there is a data race.
# Why a final reference cannot “escape” from the constructor
If the constructor publishes this (e.g., assigning it to a static field), the object’s reference may become visible before the final field is initialized, breaking the guarantee.
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample() {
i = 1; // 1 write final field
obj = this; // 2 "this" escapes
}
public static void writer() {
new FinalReferenceEscapeExample();
}
public static void reader() {
if (obj != null) {
int temp = obj.i; // 4 read final field
}
}
}Because the reference can be observed before the constructor finishes, another thread may read the default value of i (0) instead of the initialized value (1).
# Implementation of final semantics on processors
On x86, the processor does not reorder write‑write or dependent reads, so the StoreStore and LoadLoad barriers required by the JMM are effectively omitted. Consequently, final field reads and writes on x86 do not incur extra memory‑barrier instructions.
Why JSR‑133 enhanced final semantics
The original Java memory model allowed a thread to observe a final field’s default value and later see the initialized value, breaking immutability guarantees (e.g., a String could appear to change). JSR‑133 introduced the write and read reordering rules for final fields, giving programmers safe initialization without needing explicit synchronization, provided the object does not escape during construction.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
