Why Java’s final Keyword Guarantees Thread Safety – Deep Dive
This article explains the semantics of Java's final keyword, how the compiler and JVM enforce immutability and memory‑barrier rules, and why final fields provide thread‑safe, write‑once‑read‑many behavior for both instance and static variables.
Final Keyword Semantics and Usage
The compiler may move reads of final fields across synchronization barriers and cache final values in registers, making them immutable across threads.
Thread‑Safety of Final Fields
Final fields are effectively immutable; writes are only allowed in constructors, and all threads see the final value after construction.
The Happen‑Before relationship guarantees that the final value is visible to all threads without reordering.
Usage Model (write‑once, read‑many)
Final fields are assigned in the constructor; no other thread may see a partially constructed object.
This ensures that any thread reading the object observes the fully initialized final fields.
Code Example and Analysis
<code>public class FinalClass {
public final int i;
public int j;
public final DefineFinalObject defineFinalObject;
static FinalClass finalClass;
public FinalClass() {
// i = 4; // compile‑time error if placed inside try
// defineFinalObject = new DefineFinalObject();
try {
j = 9; // allowed because j is not final
} catch (Exception e) {}
}
static void writer() {
finalClass = new FinalClass();
System.out.println("have init FinalClass");
}
static void reader() {
if (finalClass != null) {
int x = finalClass.i;
int y = finalClass.j;
System.out.printf("get x = %d, and y = %d", x, y);
}
}
}
</code>The compiler forbids writing to i inside a try block and allows writing to j . Reads of i before its assignment cause a compile‑time error, enforcing the write‑once rule.
Final vs. static Final
<code>public class FinalSharedClass {
public final static int num;
public static int x;
public final static DefineFinalObject defineFinalObject;
static {
num = 10;
defineFinalObject = new DefineFinalObject();
System.out.printf("have finished static code for num=%d and obj=%s...\n", num, defineFinalObject);
}
static void writer() {
// num = 20; // compile‑time error
// defineFinalObject = new DefineFinalObject(); // compile‑time error
x = 10;
defineFinalObject.setAge(10);
}
static void reader() {
System.out.printf("read final static num: %d \n", num);
System.out.printf("read final static defineFinalObject: %s \n", defineFinalObject);
System.out.printf("read static x: %d \n", x);
}
}
</code>Static final fields are initialized in a static block, which runs once when the class is loaded, guaranteeing that all threads see the initialized value.
Implementation of Final Semantics
On AArch64 the JVM inserts a StoreStore memory barrier before a constructor returns, ensuring that writes to final fields become visible only after the barrier.
Volatile vs. Final Memory Barriers
Volatile writes use a StoreLoad barrier, while final writes use a StoreStore barrier. The former prevents reordering of subsequent reads; the latter prevents reordering of preceding writes.
Summary of the Specification
Final fields are assigned in constructors (or static blocks) and cannot be modified afterwards.
Reading a final field requires first obtaining a reference to the containing object.
Static final fields act as constants; System.in/out/err are special cases that remain mutable via System.setIn, etc.
In bytecode, final fields carry the ACC_FINAL flag (0x1000).
Xiaokun's Architecture Exploration Notes
10 years of backend architecture design | AI engineering infrastructure, storage architecture design, and performance optimization | Former senior developer at NetEase, Douyu, Inke, etc.
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.