Java final Keyword: Building Immutable Objects to Eliminate Synchronization

This article demonstrates how Java's final keyword enables immutable objects that remove the need for synchronization in concurrent code, using a config hot-reload example that shows race conditions in mutable objects and zero errors with immutable design.

samdeepthink
samdeepthink
samdeepthink
Java final Keyword: Building Immutable Objects to Eliminate Synchronization

The article begins by referencing a prior discussion on thread safety, noting that shared mutable state requires synchronization mechanisms like synchronized or volatile. It then poses the question: if an object's state is immutable, can we avoid synchronization entirely? The answer is yes — immutable objects eliminate the need for synchronization because their state never changes after construction, so no intermediate states exist that require coordination.

Analogy: Identity Card as Immutable Object

An identity card serves as a real-world analogy: once issued, its data never changes. Systems only read it; they cannot modify it. If a change is needed, a new card is issued.

Demonstrating the Problem: Mutable Config with Race Condition

A ConfigBroken class holds two fields — dbHost (String) and dbPort (int) — that must always appear as valid pairs: 10.0.0.1:3306 or 10.0.0.2:6603. An updater thread alternates between these two configurations by calling update(host, port), which assigns the two fields in separate statements. A reader thread repeatedly reads both fields and counts mismatches where host ends with .1 but port is 6603 — a combination never written.

public class ConfigBroken {
    private String dbHost = "10.0.0.1";
    private int dbPort = 3306;

    public void update(String host, int port) {
        this.dbHost = host;
        this.dbPort = port;
    }
    // ... main method with updater and reader threads
}

Running on Windows 11 with Java 17 for three trials produced mismatch counts of 13707, 1616, and 32954 out of 100 million reads. The variation (20× difference) depends entirely on thread scheduling.

Root Cause: Non-Atomic Update

The update method writes dbHost then dbPort in two steps. Between these steps, a reader can observe a partially updated object — e.g., host=10.0.0.1 with port=6603 — creating a transient invalid state. The fundamental issue is shared mutable state without synchronization.

Solution: Immutable Design with final

The fix uses a new ConfigImmutable class with three changes:

Fields declared final, no mutators: Both dbHost and dbPort are private final. The update method is removed. Values are set only in the constructor.

Replace object reference instead of mutating: The updater thread creates new ConfigImmutable instances and assigns them to the shared config reference:

config = new ConfigImmutable("10.0.0.2", 6603);
config = new ConfigImmutable("10.0.0.1", 3306);

Reference assignment is atomic — the reference either points to the old object or the new one, never a half-updated state.

Reader snapshots the reference: The reader copies config into a local variable c before reading fields:

ConfigImmutable c = config;
String host = c.dbHost;
int port = c.dbPort;

This ensures both fields come from the same object. Even if config is updated concurrently, the old object remains unchanged in memory.

Running the immutable version three times yields wrong=0 every time — zero mismatches across 100 million reads per run.

Why It Works: Three Mechanisms

Internal state frozen: final fields plus no setter methods guarantee each ConfigImmutable instance holds a complete, valid configuration forever.

Atomic reference swap: Updating the shared reference is a single step; no intermediate state exists.

Local variable snapshot: Reading from a captured local reference prevents the reader from seeing fields from two different objects.

final ≠ Immutable

A final field only prevents reassignment of the reference; the referenced object's internal state can still mutate:

private final List<String> hosts = new ArrayList<>();
hosts

cannot point to another list, but hosts.add() mutates the list contents.

Three Conditions for True Immutability

No methods that modify state after construction (no set, update, save).

All fields declared final — compiler enforces this.

Proper construction: this does not escape during construction.

Java 17 Record Shortcut

Java 17 record syntax automatically generates an immutable class with final fields, constructor, accessors, equals, hashCode, and toString:

public record ConfigImmutable(String dbHost, int dbPort) {}

The compiler adds final to the fields implicitly.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencySynchronizationthread safetyrace conditionimmutabilityfinal keywordJava 17 records
samdeepthink
Written by

samdeepthink

Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.