Recommended Free Tools
A memory model is a programming language’s contract for what concurrent code is allowed to observe. To reason reliably about shared data, identify the synchronization action that orders a write before another thread or goroutine reads it; source-code order in one thread, or an atomic access by itself, does not establish that relationship.
What a memory model guarantees
Concurrent programs can perform reads and writes in multiple threads or goroutines, while compilers and processors may reorder operations as long as the language’s rules are respected. A memory model defines which results of those executions are permitted. It gives meaning to program order, synchronization, visibility, atomic operations and data races.
As an Amazon Associate I earn from qualifying purchases.
It is a language-level contract, not a description of a particular processor’s cache behavior. Reason from the rules of the language and version you are using, rather than assuming that a write becomes visible to another thread simply because it has run first on the hardware.
What “happens-before” means
Happens-before is a way to establish ordering across concurrent execution. If action A happens-before action B, the language model orders A before B. The relationship is built from operations that are ordered within one execution thread, synchronization between threads, and transitivity: if A happens-before B and B happens-before C, then A happens-before C.
#1 Best Overall
Go’s memory model describes happens-before as the transitive closure of sequenced-before and synchronized-before relations. C++ defines it through sequencing, synchronization and transitivity. In either case, two statements that happen to be in separate threads do not gain a cross-thread ordering edge merely because one appears earlier in the source file.
A practical way to check visibility
- Find the shared data. Identify which thread writes it and which other thread reads or modifies it.
- Find the synchronization operation. Look for a channel operation, mutex, volatile or other language-defined synchronization action, or an atomic operation with appropriate ordering.
- Trace the edge. Verify that the receiving operation is connected to the publishing operation under that language’s rules. For acquire/release atomics, for example, the acquire must observe the release for the described ordering to apply.
- Check every conflicting access. If another path reads or writes the same data without the required synchronization, the fact that one path is ordered does not make all access safe.
- Check the program’s logic separately. A correctly synchronized program can still implement the wrong algorithm.
When acquire and release help
Acquire and release are useful when one thread publishes data for another without protecting every access with a lock. The publishing thread first writes the data, then performs a release operation. The receiving thread performs an acquire operation that observes that release, then reads the published data. In Rust’s documented model, the earlier operations are ordered before later operations when a Release store is observed by an Acquire load.
This is a language-level ordering and visibility guarantee, not a promise to “flush the cache.” The relevant question is whether the acquire observes the release and therefore establishes the required relationship—not whether the operations have names that sound strong.
Free tools Windows power users keep installed
One-click scans. No signup required.
Relaxed atomics are not publication by themselves
A relaxed atomic operation is still atomic: its access to that atomic object is indivisible under the language’s rules. But Relaxed imposes no ordering constraints beyond atomic operations in Rust’s documentation. It therefore does not, on its own, publish unrelated non-atomic data to another thread. Use a synchronization mechanism that establishes the needed edge, or protect the shared state with a mutex or another suitable primitive.
Are atomic variables enough to prevent data races?
No. Atomicity and ordering answer different questions. Atomicity concerns an operation on the atomic object; ordering concerns how that operation relates to other memory accesses. A program may use an atomic flag and still access associated data incorrectly if the flag operations do not establish the needed synchronization or if other conflicting accesses bypass the protocol.
Rust explicitly defines conflicting unsynchronized accesses where at least one is non-atomic as data races, and says those races are undefined behavior. Go’s advice is to serialize access to data modified while multiple goroutines can access it, using channels or synchronization primitives such as those in sync and sync/atomic. Java’s specification also cautions that freedom from data races or sequential consistency does not make a group of operations atomic.
Rank #4
As a default, use higher-level synchronization that makes ownership and access boundaries clear. Reach for low-level atomics when the protocol requires them and you can identify exactly which operation creates the ordering edge.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →How Go, Java, C++ and Rust differ
These languages all specify how concurrent operations are interpreted, but their guarantees and terminology are not interchangeable. The table summarizes the scope of the official documents cited here; it is not a substitute for the language edition or library documentation applicable to a particular program.
Best Value
| Language | Synchronization and ordering | Race and correctness notes | Document scope |
|---|---|---|---|
| Go | Channels and synchronization primitives, including sync and sync/atomic; happens-before is derived from sequenced-before and synchronized-before. |
The model says programs that modify data accessed simultaneously by multiple goroutines must serialize that access. Data-race-free programs have the DRF-SC guarantee: their outcomes can be explained by a sequentially consistent interleaving. | Official memory model dated June 6, 2022. |
| Java | The Java Language Specification defines synchronization and happens-before rules for threads and memory actions. | Sequential consistency and freedom from data races do not make a multi-operation group atomic; check whether the algorithm needs an operation to be indivisible as a whole. | Java Language Specification, Chapter 17, version 26. |
| C++ | Mutex operations, fences and atomic operations; its rules describe sequencing, synchronization, relaxed atomics, acquire/release and happens-before. | Use the applicable standard and library documentation to determine the precise behavior of the operations in use. | The cited intro.races text is from a live working draft, whose wording and clause numbering may change. |
| Rust | Synchronization types and explicit atomic orderings: Relaxed, Release, Acquire, AcqRel and SeqCst. | Conflicting unsynchronized accesses with at least one non-atomic access are data races and undefined behavior. | std::sync::atomic documentation describes C++20 atomic rules without consume ordering; the ordering documentation identifies std 1.99.0. |
Go: serialize shared mutation
Go’s documented starting point is to serialize access when multiple goroutines can access data being modified. A channel or synchronization primitive can establish the necessary relationship; the goal is not merely to make an individual read or write appear atomic. The model’s DRF-SC guarantee applies to data-race-free Go programs and should not be generalized to other languages.
Java: apply the JLS rules to the runtime you use
Java’s Chapter 17 specifies thread and memory semantics, including happens-before. Use the Java Language Specification version applicable to the runtime, and distinguish those language rules from details of a particular JVM implementation. Also verify whether a sequence of operations needs to be atomic as a group; race freedom alone does not provide that property.
C++: consult the published standard for production guidance
The live C++ working draft explains conflicting evaluations, mutex and atomic synchronization, acquire/release operations, relaxed atomics and happens-before. Because that draft can change, production decisions should be checked against the published C++ standard edition and library documentation relevant to the project.
Rust: choose an ordering for the protocol, not by name alone
Rust documents Relaxed, Release, Acquire, AcqRel and SeqCst orderings. Its atomic rules currently follow C++20’s rules except that Rust does not provide consume ordering. Choose the ordering that establishes the relationship your program needs; do not assume that every atomic operation orders unrelated accesses.
Quick Recap
A reliable checklist for concurrent code
- Name the shared state: list the data that can be accessed by more than one thread or goroutine.
- Identify conflicting operations: find pairs of accesses where at least one modifies the same state.
- Choose a synchronization mechanism supported by the language: often a mutex, channel or higher-level synchronization type is easier to reason about than a custom atomic protocol.
- Prove the ordering path: point to the operation that publishes a change and the operation that receives it.
- Keep language rules distinct: verify the exact language edition and library behavior instead of transferring assumptions from another ecosystem.
- Review logical atomicity: decide whether a group of operations must behave as one indivisible action; a race-free implementation may still need a lock or a higher-level atomic operation for that requirement.
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.




