skip to content

Why are immutable objects inherently thread-safe, and what makes a Java class truly immutable?

level: juniorimportance: must knowfreq 78%

answer

  1. final class, private final fields, no setters
  2. defensive copy in AND out
  3. no writes after construction = nothing to race on
  4. final-field JMM publication guarantee
  5. records don't deep-copy mutable components

basics

~20 s

An immutable object never changes after it is built, so no thread can modify it while another reads it. With no shared mutable state to corrupt, no locking is needed. Make fields final, set them once in the constructor, and add no setters.

solid answer

~40 s

Immutable objects are thread-safe because their state is fixed at construction and never changes; threads only ever read the same values, so there are no write-write or read-write conflicts to synchronize. To make a Java class immutable: make it final (or otherwise prevent subclassing), make all fields private final, set them only in the constructor, provide no setters or mutators, and defensively copy any mutable inputs and outputs (arrays, collections, Date) so external references can't mutate internal state. A record gives you final fields and no setters for free, but you still must defensively copy mutable components. Because the object can't change, you can freely share it across threads and use it as a HashMap key or in a Set without worrying about later updates becoming visible.

code

java · 23 lines
java
public final class ImmutablePoint {
    private final int x;
    private final int y;
    private final int[] history;          // mutable component

    public ImmutablePoint(int x, int y, int[] history) {
        this.x = x;
        this.y = y;
        this.history = history.clone();   // defensive copy IN
    }

    public int x() { return x; }
    public int y() { return y; }

    public int[] history() {
        return history.clone();           // defensive copy OUT
    }

    // 'mutation' returns a new object instead of changing this one
    public ImmutablePoint withX(int newX) {
        return new ImmutablePoint(newX, y, history);
    }
}

go deeper

for a junior

Can state that immutable means 'doesn't change after construction' and that this avoids the need for locks; knows to use final fields and no setters.

for a middle

Lists all five rules including defensive copies in and out, and distinguishes a final reference from an immutable target.

for a senior

Explains the final-field memory-model publication guarantee and why it makes immutable objects safe even through a data race; knows record copy caveats and the allocation trade-off.

for a principal

Frames immutability as a system-level strategy (shareable value objects, cache keys, event payloads), weighs allocation/GC cost vs. mutable+synchronized designs, and sets team conventions for value types.

## What 'immutable' means An object is **immutable** if its observable state cannot change after the constructor finishes. Once built, every read of any field returns the same value forever. `String`, `Integer`, `BigDecimal`, and `LocalDate` are classic examples. ## What 'thread-safe' means **Thread-safe** means many threads can use the object simultaneously without external synchronization and still get correct behavior. The classic threats are: - a **race condition**: one thread writes while another reads (or two write), producing a torn or inconsistent result; - a **visibility problem**: one thread's write is never seen by another because of CPU caches / compiler reordering (governed by the Java Memory Model). ## Why immutability removes both threats If state never changes after construction, there are **no writes after construction** to race on. Every thread sees the same fixed values, so there is nothing to corrupt and nothing to synchronize. This is why the Java docs call immutable objects *inherently* thread-safe. ## The five rules for a truly immutable Java class 1. **Don't allow subclasses** to override behavior: make the class `final` (or use a private constructor + factory). A subclass could add mutable state or override methods to leak it. 2. **Make every field `private final`.** `private` hides it; `final` forbids reassignment after the constructor. `final` also gives a memory-model guarantee (see below). 3. **Set fields only in the constructor.** No setters, no methods that change state. 'Mutating' operations must return a *new* object (like `String.toUpperCase()`). 4. **Defensively copy mutable inputs.** If the constructor receives a mutable object (an array, a `List`, a `Date`), store a copy. Otherwise the caller still holds a reference and can mutate your internals later. 5. **Defensively copy mutable outputs.** A getter that returns the internal `List` or array hands out a live reference; return an unmodifiable view or a copy instead. ## The `final` field memory guarantee The Java Memory Model gives a special rule: if an object is correctly constructed (the `this` reference does not escape the constructor), then any thread that obtains a reference to the object is **guaranteed to see the correct, fully-initialized values of its `final` fields**, with no extra synchronization. This is what makes immutable objects safe even when published through a data race. Non-final fields get no such guarantee. ## Records A Java `record` is implicitly final, generates `private final` fields, and has no setters, so it's a great base for immutability. But records do **not** deep-copy: a `record Point(int[] data)` still leaks the array. You override the canonical constructor and accessor to copy mutable components. ## Why this matters practically An immutable object can be shared freely, cached, used as a map key (its `hashCode` never changes), and passed across threads without any lock. The cost is allocation: 'changing' it means allocating a new object. For hot paths that change often, a mutable design with explicit synchronization or thread confinement may be cheaper.

  • Does making a field `final` make the object it points to immutable?
    No. `final` only prevents reassigning the reference. If it points to a mutable object (e.g. an ArrayList), that object's contents can still change. You must also defensively copy or use an immutable type.
  • Why is defensive copying needed on the way out of a getter, not just on the way in?
    If a getter returns the live internal collection or array, a caller can mutate it and thereby mutate your object's state, breaking immutability. Return a copy or an unmodifiable view.

saying these in an interview costs you the question

  • Saying 'just add final to the fields' while still returning the internal mutable list from a getter
  • Thinking `final` on a reference field makes the referenced object immutable (it only freezes the reference)
  • Believing a record is automatically deeply immutable when it holds an array or List
  • Confusing immutable with unmodifiable wrappers (Collections.unmodifiableList still wraps a mutable backing list)

context