Back to Immutability
Q4Question 4 of 4

Why is an immutable value class inherently thread-safe?

Key Concepts
  • Thread safety problems come from shared mutable state – immutable objects have no mutable state
  • Once constructed, all fields are final and never change – no race conditions possible
  • The JMM guarantees that final fields are safely published after the constructor completes
  • No synchronization, no volatile, no locks needed – zero concurrency overhead
  • Examples: String, BigDecimal, LocalDate, Integer – all immutable and freely shared across threads

Definition

An immutable object has no mutable state, so there is nothing for threads to race on. The Java Memory Model guarantees final fields are visible to all threads after construction. No synchronization needed.

Why immutable = thread-safe:

  • No shared mutable state. Thread safety bugs require mutability. If fields never change, there is no race condition.
  • Final field guarantee. The JMM ensures that final fields are safely published to all threads after the constructor completes.
  • Zero overhead. No synchronized blocks, no volatile fields, no AtomicReference needed.

A printed bank statement can be shared with any number of auditors simultaneously. None of them can modify it, so there is no conflict. A mutable spreadsheet, on the other hand, needs locking to prevent two auditors from editing the same cell at the same time.

Understanding the Concepts

Why do threads conflict on mutable state?

Thread safety problems arise when two or more threads read and write the same mutable data without coordination. The classic problem is a lost update: both threads read the same value, compute a result, and write it back, so one thread's write overwrites the other's.

JAVA
package com.bank.core;

import java.math.BigDecimal;

// MUTABLE: race condition possible
public class MutableAccount {
    private BigDecimal balance;  // shared mutable state

    public MutableAccount(BigDecimal balance) {
        this.balance = balance;
    }

    public void debit(BigDecimal amount) {
        // Thread 1 and Thread 2 both read balance = 1000
        // Both subtract 500
        // Expected final balance: 0. Actual: 500 (lost update)
        balance = balance.subtract(amount);
    }

    public BigDecimal getBalance() { return balance; }
}

To fix this mutable version, you would need synchronized blocks, volatile fields, or atomic operations – all of which add complexity and overhead.

Why do immutable objects avoid this?

An immutable object has no setter methods and all fields are final. Once the constructor finishes, the object's state can never change. Because no thread can modify it, there is nothing to race on and no coordination is needed.

JAVA
package com.bank.core;

import java.math.BigDecimal;
import java.time.Instant;

// IMMUTABLE: no race condition possible
public final class AccountSnapshot {
    private final String accountId;
    private final BigDecimal balance;
    private final Instant timestamp;

    public AccountSnapshot(String id, BigDecimal balance) {
        this.accountId = id;
        this.balance = balance;
        this.timestamp = Instant.now();
    }

    public String getAccountId() { return accountId; }
    public BigDecimal getBalance() { return balance; }
    public Instant getTimestamp() { return timestamp; }
}
JAVA
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// Safe to share across any number of threads without synchronization
AccountSnapshot snapshot = new AccountSnapshot("SAV2001", new BigDecimal("500000"));
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println(snapshot.getBalance()));  // safe
executor.submit(() -> System.out.println(snapshot.getBalance()));  // safe
executor.submit(() -> System.out.println(snapshot.getAccountId())); // safe
executor.shutdown();

No thread can modify the snapshot. Every thread sees the same immutable values, guaranteed.

What is the JMM final field guarantee?

The Java Memory Model (JMM) specifies that when a constructor completes, all final fields are guaranteed to be visible to any thread that obtains a reference to the object. This is called safe publication.

Without final, a thread might see a partially constructed object where some fields have not been assigned yet. With final, this is impossible. The JMM inserts a memory barrier at the end of the constructor that ensures all final field writes are flushed to main memory before any other thread can see the object.

How do you use immutable objects for state updates?

Since immutable objects cannot change, you update state by creating a new object. The common pattern is read-compute-swap using AtomicReference.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

public class AccountStateManager {
    private final AtomicReference<AccountSnapshot> currentState;

    public AccountStateManager(AccountSnapshot initial) {
        this.currentState = new AtomicReference<>(initial);
    }

    public void debit(BigDecimal amount) {
        currentState.updateAndGet(current ->
            new AccountSnapshot(
                current.getAccountId(),
                current.getBalance().subtract(amount)
            )
        );
    }

    public AccountSnapshot getState() {
        return currentState.get();  // always returns a consistent snapshot
    }
}

This pattern is lock-free. The AtomicReference uses compare-and-swap (CAS) internally. Each debit creates a new immutable snapshot and atomically replaces the old one.

What are common patterns using immutable objects in concurrency?

  • AtomicReference for lock-free state updates: read the current immutable state, create a new one, CAS-swap.
  • Event sourcing stores immutable events. Replaying events reconstructs state. Every event is a final, unchangeable fact.
  • Snapshot isolation in databases returns an immutable view of data at a point in time, similar to AccountSnapshot.
  • Copy-on-write collections like CopyOnWriteArrayList create a new internal array on every write, so readers always see a consistent snapshot.

What should a senior engineer know?

  • Records are great for concurrent snapshots. record AccountSnapshot(String id, BigDecimal balance, Instant time) {} is immutable, thread-safe, and compact. All record fields are final.
  • Immutable objects eliminate entire categories of concurrency bugs: lost updates, dirty reads, torn reads, and visibility problems.
  • The trade-off is allocation. Every state change creates a new object. The JVM's garbage collector handles the short-lived old copies efficiently, especially in the Young Generation.
  • Volatile is not enough for compound actions. Marking a field volatile ensures visibility but does not prevent lost updates from read-modify-write sequences. Immutability combined with AtomicReference solves both problems.
  • String, BigDecimal, LocalDate, Integer are all immutable and freely shared across threads in banking applications without any locking.

Visual Overview

DIAGRAM
  Mutable state + multiple threads = race conditions, lost updates, dirty reads
    → needs synchronized, volatile, locks

  Immutable state + any threads = safe, no conflicts, no locks
    → final fields guaranteed visible after construction (JMM)
    → zero synchronization overhead

  Pattern: AtomicReference<ImmutableState>
    read → compute new immutable state → CAS-swap → no locks

Key Takeaway

An immutable object is inherently thread-safe because there is no mutable state for threads to conflict on. Once constructed, all final fields are visible to every thread (guaranteed by the Java Memory Model), and no thread can modify them. No synchronization, no volatile, no locks needed.

This is why String, BigDecimal, and LocalDate can be freely shared across any number of threads. In banking, immutable snapshots of account state are safe for concurrent processing without any locking overhead.

One sentence to remember: immutable means no shared mutable state, no race conditions, no locks – thread-safe by construction.

---

Share
Use ← → arrow keys to navigate