skip to content

In Java, the `volatile` keyword, `synchronized` blocks, `final` fields, and the `java.util.concurrent.atomic` classes (or `VarHandle` access modes) each grant a *different subset* of the guarantees the Java Memory Model specifies. For each of those four constructs, state precisely what the specification grants and what it withholds, and give a concrete bug that follows from picking the wrong one.

level: middleimportance: must knowfreq 62%

answer

  1. volatile: 2.5 of 3 — no compound RMW
  2. synchronized: all three, same monitor only
  3. final: freeze at constructor end, `this` must not escape
  4. atomics: one variable, never two
  5. plain long/double may tear (JLS 17.7)

basics

~20 s

volatile: visibility and ordering for that field plus single read/write atomicity, but no compound atomicity. synchronized: all three, only for threads taking the same lock. final: a one-time publication freeze at constructor end, nothing else. Atomics/VarHandle: single-variable atomicity, never multi-variable.

solid answer

~50 s

**volatile** grants a happens-before edge (write to a volatile field happens-before a later read of it), so everything the writer did before is visible and ordered; it also makes single reads/writes indivisible, including `long`/`double`, which may otherwise tear. It withholds compound atomicity: `volatile int count; count++` still loses updates. **synchronized** grants all three — visibility, ordering, and mutual exclusion across *any* number of fields — but only to threads that acquire the *same* monitor. One unsynchronized reader voids the guarantee entirely. **final** grants a one-shot freeze: any thread that obtains the reference, even through a data race, sees the final fields and everything reachable from them as of the constructor's end — provided `this` did not escape during construction. It grants nothing about later mutations and no atomicity. **Atomics/VarHandle** grant atomic read-modify-write on *one* variable plus volatile-mode ordering; a two-field invariant still needs a lock or an immutable holder swapped atomically.

code

java · 9 lines
java
class Hits {
    private volatile int count;      // visible + ordered, still not atomic
    void hit() { count++; }          // read, add, write: interleaves

    // correct: single-variable RMW
    private final java.util.concurrent.atomic.AtomicInteger safe =
            new java.util.concurrent.atomic.AtomicInteger();
    void hitSafely() { safe.incrementAndGet(); }
}

go deeper

for a junior

Be able to say which construct to reach for: volatile for a flag or a published reference, synchronized or an Atomic class for a counter, final for immutable objects — and that volatile does not make ++ safe.

for a middle

Give the grant/withhold pair for each construct and name the happens-before edge behind volatile and synchronized, plus the single-variable limit of the atomics.

for a senior

Add the failure modes you have debugged: asymmetric locking, this escaping a constructor, a mutable object safely published but unsafely mutated, and when a weaker VarHandle access mode is justified.

for a principal

Discuss encoding the required guarantee in the API surface — immutable value objects swapped by one CAS instead of many mutable fields — and the cost of over-synchronizing versus the maintenance risk of hand-tuned access modes.

## The matrix, not the theory Every Java concurrency construct is a contract in the Java Language Specification that grants some guarantees and stays silent on others. Silence is not a promise that it works anyway — it means the implementation may do whatever is fastest. The way to answer this question is construct by construct: what does the spec say, what does it not say, and what breaks when you pick wrong. ## volatile **Grants.** A write to a volatile field *happens-before* every subsequent read of that same field (JLS §17.4.5). That edge does double duty: the reader is guaranteed to observe the write rather than a stale value, and it is guaranteed to observe everything the writer did *before* that write, in order. Volatile also guarantees that a single read or single write of the field is indivisible — notably for `long` and `double`, which a non-volatile access is explicitly permitted to split into two 32-bit halves (JLS §17.7), so a racy reader can observe a value that was never written. **Withholds.** Any *compound* action. `count++` compiles to a read, an add, and a write; volatile makes each step visible and ordered but does nothing about the gap between them. Two threads read 5, both write 6, one increment is gone. Volatile also gives no mutual exclusion, so it cannot protect an invariant spanning two fields even if both are volatile. **Typical wrong pick:** `private volatile int count;` with `count++` as a hit counter. It looks synchronized, passes light testing, and silently undercounts under load. ## synchronized (monitors) and Lock **Grants.** Releasing a monitor happens-before any subsequent acquire of *that same* monitor. On top of the visibility and ordering that edge provides, the monitor adds mutual exclusion, so a whole block of compound actions — check-then-act, multi-field updates, `count++` — is indivisible with respect to other holders of the same lock. This is the only construct in the list that gives all three guarantees over arbitrarily many fields. **Withholds.** Everything for threads that do not take the same lock. Locking only the writer is a classic error: an unsynchronized reader has no happens-before edge with the writer at all, so its read is a data race and may return any value the model permits, indefinitely. Different lock objects give no edge with each other. And a lock says nothing about state mutated outside it. **Typical wrong pick:** `synchronized` setter, plain getter. The getter can spin on a stale value forever, or the JIT can hoist it out of a loop. ## final fields **Grants.** The final-field semantics introduced by JSR-133 (JLS §17.5) define a *freeze* at the end of a constructor. If a thread obtains a reference to the object, it is guaranteed to see at least the values assigned to that object's final fields in the constructor, plus everything transitively reachable from those fields at freeze time — and this holds *even without synchronization on the publication itself*. That is what makes properly constructed immutable objects safe to hand around through a plain field. **Withholds.** The guarantee is void if `this` escapes during construction (registering a listener, starting a thread, publishing to a static in the constructor) — the escaping reference can be observed before the freeze. It covers only final fields, not the object's non-final fields. It is a one-shot snapshot: a final field pointing to an `ArrayList` guarantees the *reference* and the list's state as of construction, not any mutation performed afterwards. And it grants no atomicity whatsoever. **Typical wrong pick:** "the field is final so the object is thread-safe" — while the referenced collection is mutated after construction. ## Atomic classes and VarHandle access modes **Grants.** `AtomicInteger.incrementAndGet()`, `compareAndSet`, `getAndAdd` and the equivalent `VarHandle` methods provide an indivisible read-modify-write on a *single* variable, backed by a hardware CAS, with the visibility and ordering of a volatile access. `VarHandle` (Java 9+) additionally lets you *choose* the ordering strength: plain (no cross-thread ordering), opaque (bitwise atomicity and per-variable coherence, no ordering relative to other variables), acquire/release (one-directional ordering), and volatile (the strongest, matching a volatile field). **Withholds.** Atomicity across two variables. `if (atomicA.get() > 0) atomicB.incrementAndGet();` is two atomic operations and one race. Weakening the access mode weakens ordering deliberately: plain-mode reads of a field carry no publication guarantee, so a lock-free reader of a plain field can see a null or half-built object even though every individual write eventually lands. ## How to close Say it as a table in words: volatile = visibility + ordering + single-access atomicity; synchronized = all three, same-lock only; final = publication ordering, one shot; atomics = one-variable atomicity plus the ordering mode you pick. Then name the failure each omission causes.

  • A field is written inside a `synchronized` block but read without any lock. What exactly does the Java Memory Model promise that reader?
    Nothing. The happens-before edge is between a monitor *release* and a subsequent *acquire of the same monitor*; a reader that never acquires the lock forms no edge, so the read is a data race. It may observe a stale value indefinitely, and the JIT is free to hoist it out of a loop. Locking must be symmetric — every accessor of the guarded state takes the same lock, or the field must additionally be volatile.
  • You have two `AtomicInteger`s that must stay consistent with each other. Why is that not solved by making both atomic, and what do you use instead?
    Each operation is individually indivisible, but the pair of operations is not: another thread can observe or modify the second variable between your two calls, so any invariant spanning both can be broken. The fixes are to guard both under one lock, or to fold both values into a single immutable holder object and swap it with one `AtomicReference.compareAndSet`, which makes the pair change atomically.
  • When does the `final`-field guarantee fail to apply?
    When `this` escapes during construction — for example the constructor registers the object as a listener, starts a thread that captures it, or assigns it to a static field before returning. Another thread can then reach the object before the freeze and observe default values in its final fields. It also never covers non-final fields, and it covers a referenced mutable object only as of the constructor's end, not later mutations.

saying these in an interview costs you the question

  • Saying `volatile` makes `count++` atomic, or that it 'locks' the field
  • Synchronizing only the writer and assuming readers get the value because 'the release flushes the cache'
  • Treating `final` as making the referenced object immutable, so later mutations of a final `List` field are thread-safe
  • Claiming two atomic variables give an atomic two-variable invariant
  • Relying on 64-bit hardware so a plain `long` 'cannot tear', instead of on what the specification grants

context