skip to content

A field is declared `final` and holds a reference to a `HashMap` that is later modified by several threads. What exactly does the memory model guarantee here, and what does it not?

level: middleimportance: should knowfreq 42%

answer

  1. freezes the field, not the object
  2. state as of freeze time only
  3. final blocks rebinding, not mutation
  4. HashMap resize corruption is orthogonal
  5. copyOf in constructor, or a concurrent map

basics

~20 s

It guarantees the reference itself, and the map contents as of the end of the constructor, are visible to any thread that obtains the object. It guarantees nothing about entries added or changed afterwards — those are unsynchronized races.

solid answer

~50 s

The frozen-field rule freezes the **field**, not the object it points at. Two things follow: - **Guaranteed:** every thread that obtains the object's reference after construction sees the same non-null map reference, and sees the map's state at least as up to date as it was when the field was frozen. Entries put in by the constructor are visible without any synchronization. - **Not guaranteed:** anything that happens to the map after construction. Concurrent `put` on a plain `HashMap` is a data race with no ordering edge, so readers may see stale entries, may miss entries entirely, and — because `HashMap` is not internally synchronized — may hit corrupted internal structure or an infinite loop during a concurrent resize. So `final` gives safe **publication** of the field, not thread safety of the referenced object. If the contents must change after construction, you need a genuinely concurrent map or explicit synchronization; if they must not, make the map unmodifiable at construction time so the guarantee actually means something.

code

java · 9 lines
java
final class Config {
    private final Map<String, String> unsafe;   // caller keeps a mutable handle
    private final Map<String, String> safe;     // snapshot taken during construction

    Config(Map<String, String> src) {
        this.unsafe = src;                      // publication guaranteed, contents not
        this.safe   = Map.copyOf(src);          // frozen contents, no mutation path
    }
}

go deeper

for a junior

Say clearly that final stops the field being pointed at something else; it does not stop the map's contents from changing.

for a middle

Split the answer into guaranteed (reference plus constructor-time contents) versus not guaranteed (later mutations), and mention that HashMap under concurrent writes can corrupt structurally, not just go stale.

for a senior

Add the defensive-copy discipline — copy in, copy or view out — and explain when to reach for a concurrent map or a lock instead, noting the final field still buys safe publication of the container.

for a principal

Frame publication and mutation as separate design concerns, and set the class-level rule: immutable value types take snapshots at construction, mutable shared state gets an explicit concurrency strategy that is documented on the type.

## Freezing a field is not freezing an object graph The frozen-field guarantee is a statement about a **field's value**. When a constructor assigns a reference to a final field, the freeze at the end of the constructor guarantees that any thread obtaining the object afterwards reads *that reference*, never `null` or a stale slot. The specification adds one step of reach: a reader also sees versions of objects and arrays referenced by the final field that are **at least as up to date** as the final field itself was at freeze time. That is what makes a constructor-populated array or collection visible without synchronization. What the wording does not say — and deliberately so — is anything about writes that happen to that object *after* the constructor returns. Those are ordinary writes to ordinary objects, and they need ordinary ordering to be visible. ## The two failure modes with a shared mutable map When threads keep mutating a `HashMap` behind a final field, two separate problems stack: 1. **Visibility.** With no happens-before edge between a thread that puts and a thread that gets, the reader may simply never observe the entry. It can read the map repeatedly and keep seeing the old contents; the JIT is entitled to hoist the read of an unchanging field out of a loop. 2. **Structural corruption.** `HashMap` is not thread-safe by design. Concurrent `put` calls can interleave inside a resize and leave the table inconsistent: entries lost, entries duplicated, or, in some versions, a lookup that never terminates because a bucket chain became circular. This is not a memory-model subtlety; it is the data structure's own invariants being violated. Marking the field `final` has no bearing on it whatsoever. ## Common misreading in interviews Candidates often say "the field is final so the map is effectively immutable". `final` on a reference field constrains **rebinding** — the field cannot be reassigned to a different map — and gives the publication guarantee. It says nothing about the referenced object's mutability. A second misreading is that the deep-freeze clause makes the whole reachable graph permanently safe. It does not: the clause is about the state as of the freeze, a one-time snapshot in the ordering sense, not an ongoing property. ## Making the guarantee mean something If the intent is an immutable value: - Populate the collection inside the constructor and wrap it so it cannot be modified afterwards — for example assign `Map.copyOf(source)` or `List.copyOf(source)` to the final field. The copy is made during construction, so it falls under the freeze, and there is no mutation path afterwards to race on. - Never store a caller-supplied mutable collection directly; the caller retains a reference and can mutate it behind your back after construction, outside the frozen state. - Never hand the internal collection out from a getter; return an unmodifiable view or a copy, or you have re-opened the mutation path from the other side. If the contents genuinely must change over time, the frozen-field guarantee is the wrong tool. Use a concurrent map, or guard the map with a lock so that every reader and writer shares an ordering edge. Note that with a concurrent map the final field is still useful — it publishes the map reference safely — but the map's own guarantees, not the freeze, are what make the entries visible. ## The rule to remember Frozen fields give you a fully built object at the moment of publication. Everything that happens to the object graph after that moment needs its own ordering. Publication and mutation are two separate problems, and `final` solves only the first.

  • If the map is a `ConcurrentHashMap` behind a final field, what is each mechanism doing?
    The final field guarantees that every thread obtaining the enclosing object sees the map reference, never null, without synchronization. The map's own internal ordering then guarantees that entries written by one thread become visible to readers. They are complementary: the freeze handles publication of the container, the concurrent map handles ongoing visibility of its entries.
  • Is it enough to wrap a caller-supplied map with an unmodifiable view in the constructor?
    No. An unmodifiable view forbids modification through the view, but the caller still holds the original map and can mutate it, and those mutations show through the view with no ordering guarantee. Copy the contents during construction — for example with `Map.copyOf` — so the state the reader sees is genuinely the frozen state.

A frozen field is a photograph handed to everyone at the end of construction. Everyone gets a correct photo; nobody gets a live feed of what the subject does afterwards.

saying these in an interview costs you the question

  • Saying a final field makes the referenced object immutable or thread-safe.
  • Believing the deep-freeze clause protects post-construction mutations.
  • Storing a caller-supplied mutable collection in a final field and calling the class immutable.
  • Returning the internal collection directly from a getter while claiming immutability.
  • Treating concurrent `HashMap` corruption as a memory-model visibility issue rather than a broken-invariant issue.

context