Skip to content

VOL 09 / CH 02 / LESSON 03

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

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?

Although individual Java int reads and writes are atomic, count++ is a read-modify-write sequence and can lose updates. Declaring the field volatile supplies visibility and ordering guarantees without making that sequence atomic.

Happens-before Establishes a Visibility Contract ​

In the Java Memory Model, happens-before constrains which writes a read may observe. For a single writer with no competing writes, a write that happens-before a later read makes its value visible. With multiple writers, there is no universal “latest wall-clock value” rule: a read cannot observe a write that happens after it, or skip an intervening write ordered between the observed write and the read. Other consistency rules also apply. Common sources of happens-before edges include:

  • The program order within the same thread;
  • A monitor unlock happening before a subsequent lock on the same monitor;
  • A volatile write before subsequent reads of that field in synchronization order;
  • Actions taken before a Thread.start call happening before any actions in the newly created thread;
  • All actions of a thread before another thread returns from a join that detects its termination.

Happens-before is a partial order, not a wall-clock ordering. Conflicting accesses from different threads, at least one being a write, form a Java data race when they are not ordered by happens-before. Apparent wall-clock order does not establish that relation.

Safe publication is part of object correctness ​

A volatile reference, a shared lock, an appropriate thread-safe collection, or class initialization can publish constructed state through defined ordering. Correctly constructed final fields also have special initialization guarantees: a thread that obtains the reference can see those initialized fields even through some otherwise racy publication paths. This is not a general synchronization edge, and leaking this during construction can lose that protection.

Use a defined publication mechanism to make reference delivery and non-final state reliable. Immutability reduces the need for later synchronization; final on a list reference does not make its elements immutable.

Verify a Happens-Before Path ​

Save this as PublicationDemo.java and run java PublicationDemo.java with JDK 17 or later:

java
class PublicationDemo {
    static int value;
    public static void main(String[] args) throws InterruptedException {
        Thread writer = new Thread(() -> value = 42);
        writer.start();
        writer.join();
        System.out.println(value);
    }
}

The output must be 42: the write precedes the writer’s termination, join detects that termination, and the read follows the return. value does not need to be volatile for this particular sequence. Remove join and there is no such guarantee; observing 42 once does not validate the unsynchronized version. Adding a sleep merely gives the thread more time. It does not buy a synchronization contract.

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 constrains the allowed behavior of racy executions, but the results can be counterintuitive and need not cause a crash. In C and C++, a data race on ordinary non-atomic storage has undefined behavior. Rust's safety type system prevents ordinary safe code from producing data races, though logical races, deadlocks, and async cancellation remain possible.

In a GIL-enabled CPython build, the GIL limits simultaneous execution of Python bytecode in an interpreter; it does not make a multi-step business operation transactional. Extension code can release it. CPython also offers free-threaded builds, so check the build and runtime configuration before assuming a GIL exists. Use locks, queues, and a clear shared-state protocol as required.

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