Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk9 min

Concurrency Programming (3): Mutexes — Atomicity, Visibility, and Ordering at the Language Level

A mutex gives you mutual exclusion and an unlock-to-lock happens-before edge, not cache flushing. This guide traces the chain and compares Go, Java, C++ and Rust.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A mutex does two jobs that are easy to blur together. It stops two threads from being inside the same critical section at once. It also creates a synchronization edge: everything a thread did before it unlocked is ordered before everything another thread does after it later locks the same mutex. The first job gives you atomicity of a whole critical section. The second gives you visibility and ordering. Neither extends to data you touch outside the lock, and neither is “flush the caches” in any sense a language specification promises.

This article builds the reasoning tool that makes those claims checkable, happens-before. It then shows how Go, Java, C++ and Rust phrase the mutex rule, why an atomic variable is not the same thing as ordering, and where each language’s data-race consequences differ.

As an Amazon Associate I earn from qualifying purchases.

What locking a mutex actually guarantees

Treat a mutex contract as three separate promises:

  • Mutual exclusion. At most one thread holds the mutex at a time, so critical sections guarded by it do not overlap.
  • Synchronization. An unlock is ordered before a later successful lock of that same mutex. This is the part that carries writes from one thread to another.
  • Nothing else. The mutex has no knowledge of which variables “belong” to it. Protection exists only for accesses that all go through the same locking discipline.

Mutual exclusion alone would not be enough. If a compiler or processor were free to move a write out of the critical section, or the other thread were allowed to read a stale value after acquiring the lock, exclusion would still hold and the program would still be wrong. The synchronization rule is what rules those outcomes out.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Happens-before: the tool for reasoning about visibility

Languages with a formal memory model define a happens-before relation. The Go memory model, for instance, defines it as the transitive closure of two relations: sequenced-before (the order of statements within one goroutine) and synchronized-before (the cross-thread edges created by synchronization operations). See The Go Memory Model. If write W happens-before read R, R is guaranteed to be able to observe W (or something later), not an older value.

The four-link chain

To show that a read sees a write protected by a mutex, you need to point to every link:

  1. Thread A performs the write.
  2. Thread A releases (unlocks) the mutex, after the write in program order.
  3. Thread B later acquires the same mutex successfully, and that acquisition comes after A’s unlock in the mutex’s lock order.
  4. Thread B performs the read, after the acquisition in program order.

Links 1→2 and 3→4 are program order inside each thread. 2→3 is the mutex’s synchronization rule. Transitivity joins them, so write happens-before read. If any link is missing, there is no guarantee.

A worked Go example

var (
    mu    sync.Mutex
    data  int
    ready bool
)

// goroutine A
mu.Lock()
data = 42
ready = true
mu.Unlock()

// goroutine B
for {
    mu.Lock()
    if ready {
        fmt.Println(data) // guaranteed to print 42
        mu.Unlock()
        return
    }
    mu.Unlock()
}

Why does B see 42? B observed ready == true, which can only happen if B’s Lock came after A’s Unlock in the mutex’s order. That supplies link 3. Both writes sit before the unlock in A, and the read of data sits after the lock in B, so the chain is complete.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Notice what is not promised. If B’s first Lock happens before A’s, B sees ready == false, and there is no edge in that direction. That is fine: B simply loops and tries again. The edge only exists once the later acquisition really is later.

The same program with one access outside the lock

Suppose B reads ready without taking the mutex. Now B’s read is a conflicting, unsynchronized access to a variable A writes. There is no chain, so B may see ready == true and still read a stale data, or may never see the update at all. The mutex exists in the program, but it protects nothing about that access.

The mutex rule in four languages

Each language states the unlock-to-lock edge in its own vocabulary. The table sticks to what the cited sources state.

Language / primitive Event that synchronizes Source and caveat
Go, sync.Mutex / sync.RWMutex Call n of l.Unlock() is synchronized before call m of l.Lock() returns, for n < m. The Go Memory Model, a live page with no separate version number stated.
Java, monitor / synchronized An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (block or method entry) of that same monitor. Oracle, Java SE 8 java.util.concurrent documentation. This is Java SE 8 documentation, not a statement about the latest release.
C++, std::mutex lock() behaves as an acquire operation and unlock() as a release operation. cppreference, std::memory_order (a secondary reference). The normative wording is in the mutex requirements of the C++ working draft, a continuously updated draft rather than a final published standard.
Rust, std::sync::Mutex Not stated in the sources cited here. Check the current std::sync::Mutex documentation for its exact wording. The cited Rust core::sync::atomic documentation covers atomics and the memory-model rules they follow, not the mutex API.

Does a successful acquisition matter?

Yes. The edge is created by a successful lock. For try-style operations, a failed attempt acquires nothing, so it orders nothing. The Go memory model treats a successful TryLock as equivalent to Lock and gives an unsuccessful one no synchronizing effect. Do not infer from a failed try_lock or TryLock that the data you wanted is up to date.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Same object, not just same type

All of the rules above name the same mutex or monitor. Two different mutexes guarding the same variable give you two disciplines that do not synchronize with each other. Locking m1 in one thread and m2 in another leaves the accesses unordered, however consistent each thread looks in isolation.

Atomicity is not ordering

The word “atomic” covers several properties that people tend to bundle:

  • Indivisibility: a single operation on one object is never seen half-done (no torn reads or writes).
  • Per-object modification order: all threads agree on the sequence of values a given atomic object takes.
  • Cross-location ordering: whether other reads and writes, to other memory, are ordered around the atomic one. This is the part that depends on the memory order you choose.

C++’s memory_order_relaxed shows the gap precisely. According to cppreference, relaxed operations are atomic and obey modification-order consistency, but they are not synchronization operations and impose no ordering on concurrent accesses to other memory. Compare these two sketches:

int payload = 0;                 // plain, non-atomic
std::atomic<bool> ready{false};

// thread A
payload = 42;
ready.store(true, std::memory_order_relaxed);   // atomic, but orders nothing else

// thread B
while (!ready.load(std::memory_order_relaxed)) {}
use(payload);                    // data race: no happens-before from A's write

Every access to ready is atomic, yet the program is still broken, because payload is read and written without any synchronization edge. Changing the store to memory_order_release and the load to memory_order_acquire adds that edge, and the plain payload access becomes safe. A mutex achieves the same effect in a bundled way: unlock is the release, lock is the acquire, and the critical section is the region they bracket.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Atomics and mutexes are different tools

An atomic variable protects one object. A mutex protects an invariant that spans several objects, such as “balance equals the sum of entries“. Making each field individually atomic does not make a multi-field update atomic as a whole, and it does not stop another thread from observing the fields in an intermediate state.

Acquire/release versus sequential consistency

These are two different strengths of guarantee, and mixing them up leads to wrong conclusions in both directions.

  • Acquire/release is pairwise. It creates an edge between a specific release and a specific later acquire on the same object. Mutexes are specified in these terms in C++, per cppreference: lock is acquire, unlock is release.
  • Sequential consistency for atomics (memory_order_seq_cst in C++) additionally constrains the sequentially consistent operations to fit a single total order that all threads agree on. It applies to those selected atomic operations, not to every operation in the program.

A mutex therefore does not imply a single global timeline of everything. Two threads using two different mutexes on unrelated data have no ordering relationship at all. What the mutex gives you is local: an ordered chain through that one lock.

There is a useful practical consequence in Go. The memory model guarantees that a data-race-free program behaves as if goroutines were interleaved sequentially (the DRF-SC guarantee). So if every shared access is correctly synchronized, you may reason about the program as a simple interleaving, without reasoning about reorderings. The mutex is your means of becoming race-free; the sequential-consistency view is the reward.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

What happens when you get it wrong

The consequence of a data race is defined per language, so do not carry one language’s terminology to another.

  • Go. The memory model describes which values a racy read is permitted to observe, and it gives race-free programs the sequentially consistent interleaving guarantee described above (The Go Memory Model).
  • Rust. The atomic module documentation says Rust atomics currently follow the C++20 atomic rules, without consume ordering. Each atomic access takes an Ordering that controls how it interacts with happens-before. It also states that conflicting unsynchronized accesses, where at least one is non-atomic, are a data race and undefined behavior.
  • C++. The Rust documentation’s statement that its rules follow C++20 atomics is a reminder that the two models are closely related, but this article does not rely on that to assign C++ consequences beyond what the cited C++ pages say.
  • Java. The cited Java SE 8 page lists the happens-before rules. It does not by itself describe the outcomes of racy programs, so consult the Java Language Specification’s memory-model chapter for those.

One further Java detail from the same page: a write to a volatile field happens-before every subsequent read of that field. That produces visibility without mutual exclusion, which is another reminder that exclusion and ordering are separable.

Common misconceptions, corrected

“A mutex flushes the CPU caches”

No language specification phrases the guarantee that way. Hardware uses coherence protocols, fences, store buffers and instructions that differ between architectures. Those are implementation details a compiler uses to deliver the contract. Reason from happens-before, and the hardware story takes care of itself. Reasoning from a cache picture also misses compiler reordering and register caching, which a memory model constrains as well.

“If one access is locked, the data is protected”

Protection comes from every conflicting access following the same discipline. One unlocked read or write is enough to create a race on the whole variable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“An atomic makes the algorithm thread-safe”

An atomic prevents tearing and gives its own object a coherent history. It says nothing about other memory unless the ordering you picked creates a happens-before edge, as the relaxed example above shows.

“Two correct critical sections make a correct operation”

Locking separately for the check and the update leaves a gap between them:

mu.Lock(); ok := balance >= amount; mu.Unlock()
if ok {
    mu.Lock(); balance -= amount; mu.Unlock()   // balance may have changed in between
}

Each critical section is race-free and the combined operation is still wrong. Hold the lock across the whole check-then-act sequence.

“Every language treats races the same way”

See the previous section: Go and Rust in particular attach different consequences to racy programs, so the claim “a race is just a stale read” is not portable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A checklist for reasoning about a shared variable

  • List every access to the variable, reads and writes, in every thread and goroutine. Do any bypass the lock?
  • Is it the same mutex object (or monitor) at every access?
  • For each reader, can you name the write it must see and trace the chain: write → unlock → later successful lock → read?
  • Does the invariant span several variables? If so, one critical section must cover every read and write that depends on it.
  • If you use atomics, which ordering is requested on each access, and is any non-atomic data relying on those atomics for ordering? If so, relaxed is not enough.
  • Are you relying on a try-lock or timed lock? Only the successful path synchronizes.
  • Does your conclusion come from the language’s documented rule, not from how the code looks or how a particular machine behaves today?

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.