Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 40

Final Fields and Safe Publication

Recommended resources

Introduction

In our last lesson, we connected the abstract guarantees of the Java Memory Model to the physical reality of CPU caches and coherence protocols. You learned that volatile is the command that instructs the hardware to enforce strict ordering and visibility, ensuring that changes to a variable are propagated across threads in a timely and predictable manner. This is essential when dealing with shared, mutable state.

This lesson addresses a related but distinct challenge: how do we ensure an object is seen by all threads in its fully constructed, initial state? This is the problem of safe publication. We will explore how the final keyword provides a powerful and elegant solution, offering strong memory visibility guarantees for creating thread-safe immutable objects without the need for explicit locks or volatile fields. This will complete our exploration of the Java Memory Model.


The Problem: Publication and Reordering

When we create an object and make it available to other threads—a process called "publication"—we intuitively expect other threads to see it in a consistent, fully-formed state. However, the same compiler and CPU reorderings we've discussed can shatter this expectation.

Consider the steps involved in new MyObject():

  1. Allocate memory for the object.
  2. Initialize the fields by running the constructor's code.
  3. Assign the reference of the newly allocated memory to a variable.

Without synchronization, the JMM is free to reorder steps 2 and 3. A thread might execute them in this order:

  1. Allocate memory.
  2. Assign the reference to the variable.
  3. Initialize the fields.

If another thread reads the variable after step 2 but before step 3, it sees a non-null reference but to a partially constructed object. The fields of this object might still hold their default values (0, false, or null). Using this object can lead to NullPointerExceptions or calculations based on incorrect data.

The following diagram illustrates this exact scenario.

This sequence diagram shows how instruction reordering can cause a concurrency bug. Thread-1 allocates an object but publishes the reference to memory before initializing its fields. Thread-2 reads this non-null reference and begins to use a partially constructed object, leading to unpredictable behavior.

This issue was a critical flaw in early versions of the JMM. The following article from the JSR-133 FAQ explains how, under the old model, even immutable objects like String could appear to change their value due to these reordering issues.

JSR 133 (Java Memory Model) FAQ

This FAQ, written by the experts who revised the Java Memory Model, provides essential context.

Read the sections What was wrong with the old memory model? and How can final fields appear to change their values? to understand the severity of the problem that the new JMM needed to solve.


The final Field Guarantee: A Powerful Solution

To solve this problem, the new Java Memory Model (since Java 5) introduced special guarantees for final fields. These guarantees are at the heart of writing simple, robust, and thread-safe immutable classes.

The core guarantee is:
Once an object's constructor finishes, any thread that sees a reference to that object is guaranteed to see the correctly initialized values of that object's final fields.

This is a one-way guarantee. It prevents a thread from seeing the initial default value of a final field; it will always see the value that was set in the constructor. Furthermore, this visibility guarantee is transitive: if a final field holds a reference to another object (e.g., a final List<String>), the state of that referenced object/collection as of the end of the constructor is also guaranteed to be visible.

The "Proper Construction" Caveat

This powerful guarantee comes with one critical condition: the object must be properly constructed. This means that the this reference must not escape from the constructor before it has finished.

An object reference "escapes" if it is made visible to another thread from within the constructor. Examples include:

  • Assigning this to a static field.
  • Registering this as an event listener with another object.
  • Starting a thread from the constructor that uses the this reference.

If the reference escapes, another thread could see the object before the constructor completes, and the final field guarantees are void.

The JSR-133 FAQ clearly explains these rules.

JSR 133 (Java Memory Model) FAQ

Let's return to the FAQ to see the official explanation of the solution.

Read the section How do final fields work under the new JMM? Pay close attention to the definition of a "properly constructed" object and the code examples that distinguish correct from incorrect usage.


How it Works: The Constructor "Freeze Action"

So, how does the JVM enforce this guarantee? It comes down to memory barriers, just like with volatile, but applied in a different context.

When the JIT compiler processes a constructor, it inserts a special memory barrier at the very end. Conceptually, this is a "freeze action". This barrier acts as a one-way fence:

  • All writes to final fields inside the constructor are guaranteed to happen before this fence.
  • The publication of the object's reference (i.e., the write that makes it visible to other threads) is guaranteed to happen after this fence.

This fence effectively prevents the reordering of field initializations and reference publication that we saw in the diagram, ensuring that any thread obtaining the reference will see the "frozen" state of all final fields.

The following video provides an excellent, clear explanation of this mechanism.

The Java memory model explained, Rafael Winterhalter

In "The Java memory model explained", Rafael Winterhalter breaks down the final field modifier's role.

Watch the segment from this timestamp. He perfectly illustrates how an object allocation is separate from the constructor call (init method in bytecode) and how the final keyword introduces a "freeze action" at the end of the constructor to guarantee visibility and prevent observing default values like zero.


Safe Publication and Safe Initialization

With this understanding, we can introduce two precise terms coined by JVM expert Aleksey Shipilëv, which are invaluable for discussing concurrency.

  1. Safe Publication: This refers to any technique that ensures an object reference and the object's state are made visible to other threads correctly. Using volatile for the reference, using a synchronized block, using AtomicReference, or publishing an immutable object with final fields are all methods of safe publication.

  2. Safe Initialization: This is a property of a class. A class is "safely initialized" if all its fields are final and it is properly constructed (no this escape). The key benefit is that an instance of such a class can survive even unsafe publication. A thread might see null for a while, but once it sees the object reference, it is guaranteed to see the correct values of its final fields. This makes it a powerful "defense in depth" strategy.

The following article provides excellent code examples, including the FinalWrapper pattern, which uses these principles to safely implement lazy initialization.

Safe Publication and Safe Initialization in Java

This article by Aleksey Shipilëv is a classic deep-dive into the practical application of final field semantics.

First, read the subsection explaining publication via final fields. Start at the paragraph "There is another way..." to see the FinalWrapperFactory example and the explanation of the "freeze action". Next, read the section Safe Initialization. This part defines the concept and explains how it makes an object robust, even if it's published incorrectly.

By making your shared data transfer objects immutable with final fields, you eliminate a whole class of concurrency bugs at the source. There's no need for locks or volatile because there is no mutable state to protect after construction.


Conclusion

In this final lesson on the Java Memory Model, you've learned about one of its most powerful and practical guarantees. By understanding how to safely publish objects, you can write cleaner, more robust, and highly performant concurrent code.

Key Takeaways:

  • Unsafe publication is a real danger where instruction reordering can cause threads to see and use partially constructed objects.
  • The final keyword provides a strong visibility guarantee: if an object is properly constructed (no this reference escapes the constructor), all threads will see the final-set values of its final fields once they obtain a reference to it.
  • This is enforced by a memory barrier (a "freeze action") at the end of the constructor, which prevents initialization writes from being reordered past the publication of the reference.
  • Making shared objects immutable with final fields is a cornerstone of safe concurrent programming in Java, often eliminating the need for explicit synchronization.
  • Safe Initialization is a "defense in depth" strategy that makes objects robust even in the face of racy, unsafe publication.

This concludes our deep dive into the JVM's memory model. We've journeyed from bytecode and class loading through garbage collection and now to the intricate rules governing thread interaction. In our next module, we will move up a level of abstraction to explore Reflection and Dynamic Proxies, powerful mechanisms that allow you to inspect and manipulate Java code at runtime.