Immutable objects are inherently thread-safe because their state never changes after construction: with nothing to mutate, there is no race, no visibility gap, and no need for any synchronization. All threads always see the same, correct, fully-built state.
Why immutability buys thread safety for free
A data race requires concurrent access where at least one access is a write. An immutable object has no writes after construction, so the race condition simply cannot exist. As long as the object is published safely (see safe publication), every thread observes the same final values forever. This is why String, Integer, BigDecimal, and records are safe to share without locks.
The recipe for an immutable class
To make a class properly immutable:
- Make all fields
final. This gives JMM safe-publication guarantees: a thread that sees a reference to the object is guaranteed to see correctly-initialized final fields, even without synchronization. - Provide no setters — no method may modify state.
- Make the class
final(or all constructors private) so a subclass can't add mutable state or override behavior. - Defensively copy mutable inputs and outputs. If you hold a
Date, array, orList, copy it on the way in and on the way out, or expose only an unmodifiable view — otherwise callers can mutate your internals. - Don't let
thisescape during construction. No registering listeners or starting threads from the constructor.
public final class Point {
private final int x;
private final int y;
private final int[] tags; // mutable type → defensive copy
public Point(int x, int y, int[] tags) {
this.x = x;
this.y = y;
this.tags = tags.clone(); // copy in
}
public int x() { return x; }
public int[] tags() { return tags.clone(); } // copy out
}
A record does most of this for you — final fields, no setters, a final class — but you still must defensively copy mutable components in a compact constructor.