Skip to content

2.3 Language Memory Model: Atomicity, Visibility, and Happens-Before

Two threads read and write in sequence on paper, but in practice, they sometimes observe stale values. In the Developer Workshop, Master Chen asks you to rereason about the possible execution outcomes permitted by the language itself.

CPUs, compilers, and JIT compilers can reorder operations and use caches and registers. A single thread only needs to maintain an as-if-serial observable behavior. In the multithreaded case, without synchronization, one thread does not necessarily see writes in the order they appear in the source code.

Three Questions to Address Separately

text
Atomicity: Is an operation observed as indivisible?
Visibility: When must one thread see a write from another thread?
Ordering: Which operations are prohibited from being reordered, or must be observed in a specific order?

count++ Even a single read or write of an int is atomic, the read-modify-write sequence is composed of multiple steps and is not atomic increment. volatile It provides specific visibility and ordering guarantees, without automatically treating composite operations as transactions.

Happens-before Establishes a Visibility Contract

In the Java Memory Model, if a write operation W happens-before a read operation R, then R must observe either W or any write that occurs after W. Common examples include:

  • The program order within the same thread;
  • A monitor unlock happening before a subsequent lock on the same monitor;
  • A volatile write happening before a subsequent read of the same field;
  • Actions taken before a Thread.start call happening before any actions in the newly created thread;
  • All actions of a thread happening before the return from a successful join operation.

Happens-before is a partial order, not a wall-clock ordering. Two accesses may appear in time to occur in sequence, but if no happens-before relationship exists between them, the program may still suffer from a data race.

Safe publication is part of object correctness

A constructed object must be published through synchronization mechanisms: final field construction semantics, volatile references, locks, thread-safe collections, or class initialization. Leaking this from a constructor to other threads compromises the guarantees provided by final fields and similar constructs.

Immutable objects reduce the need for subsequent synchronization, yet they still require safe publication. final only prevents reassignment of fields; it does not automatically make a mutable list referenced by those fields immutable.

Atomics Require Understanding of Operation Semantics

CAS loop:

text
read old
compute new
compareAndSet(old, new)
if failed, retry

It is suitable for simple state transitions, but under high contention it can fail repeatedly. Issues like ABA problems, object lifecycle management, and multi-field invariants still require additional design. Atomic classes do not equate to linearizable combinations of arbitrary operations.

Lower-level languages provide relaxed, acquire, release, and sequentially-consistent ordering. Weaker ordering gives implementations more optimization freedom, but also makes correctness harder to prove. Unless there is a well-defined baseline and formal invariants, prefer locks, channels, or established concurrent data structures.

Language Guarantees Against Data Races Differ

Java's data race behavior is well-defined but counterintuitive, programs don't necessarily crash. In C/C++, ordinary memory data races typically result in undefined behavior. Rust's safety type system prevents ordinary safe code from producing data races, though logical races, deadlocks, and async cancellation remain possible.

CPython's GIL only limits the parallel execution of Python bytecode within the same interpreter and does not turn multi-step business operations into atomic transactions. Extension modules can release the GIL, and other implementations behave differently. The GIL should not be used as a substitute for lock-based queues or shared state design.

Concurrency Correctness Verification

  • Identify shared variables and the sole mechanism that protects them;
  • Define linearization points for state transitions;
  • Establish happens-before relationships for publish, close, and cancel paths;
  • Use stress testing, race detectors, and model checking to uncover rare race conditions;
  • Avoid relying on sleep to infer thread ordering;
  • Before performance optimization, retain a simple, provably correct lock implementation as a baseline.

The next chapter compares several higher-level concurrency models and examines how they constrain shared state and interaction patterns.

References

Built with VitePress | Software Systems Atlas