What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
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.
| # | Preview | Product | Price | |
|---|---|---|---|---|
| 1 |
|
C++ Concurrency in Action | $58.90 | Buy on Amazon |
| 2 |
|
Concurrency in C# Cookbook: Asynchronous, Parallel, and Multithreaded Programming | $31.55 | Buy on Amazon |
| 3 |
|
Grokking Concurrency | $49.99 | Buy on Amazon |
| 4 |
|
Rust Atomics and Locks: Low-Level Concurrency in Practice | $33.13 | Buy on Amazon |
| 5 |
|
Java Concurrency in Practice | $6.54 | Buy on Amazon |
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.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →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.
#1 Best Overall
The four-link chain
To show that a read sees a write protected by a mutex, you need to point to every link:
- Thread A performs the write.
- Thread A releases (unlocks) the mutex, after the write in program order.
- Thread B later acquires the same mutex successfully, and that acquisition comes after A’s unlock in the mutex’s lock order.
- 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.
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.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsSame 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.
Rank #3
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.
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_cstin 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.
Recommended Free Tools
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.
Best Value
- 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
consumeordering. Each atomic access takes anOrderingthat 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.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →“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.
Quick Recap
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.




