Concurrency Programming (5): Atomics — Atomicity, Visibility, and Ordering at the Language Level
Continues with counter and ready to explain the language-level rules behind Java AtomicInteger, Go sync/atomic, and CPython's application-level Atomic boundary.

Table of Contents
- 0. Continue from the Previous Article
- 1. What Guarantees Do the Atomic Rules in Each Language Provide?
- 2. Next: How Are Atomics Implemented?
0. Continue from the Previous Article
This article discusses Atomics directly from the rules defined by language memory models and public APIs.
1. What Guarantees Do the Atomic Rules in Each Language Provide?
1.1 Java: AtomicInteger
Start with the rules.
AtomicInteger.incrementAndGet() states:
“Atomically increments the current value, with memory effects as specified by
VarHandle.getAndAdd.”
This is the Atomicity guarantee: reading the old value, adding one, and writing the result back happen as one indivisible atomic read-modify-write operation.
For Visibility and Ordering, ordinary reads and writes through AtomicInteger have volatile semantics: get() uses VarHandle.getVolatile, while set() uses VarHandle.setVolatile.
JLS §17.4.4 Synchronization Order states:
“A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread.”
So a volatile write to an Atomic variable can establish a synchronizes-with relationship with a later volatile read in another thread.
JLS §17.4.5 Happens-before Order further states:
“If one action happens-before another, then the first is visible to and ordered before the second.”
That happens-before relationship gives us both Visibility and Ordering: earlier writes become visible to later reads, and those operations must be observed in the order required by happens-before.
Now apply those rules to the same two examples used earlier in the series.
Atomicity
With an ordinary increment:
Thread A Thread B
count++ count++
both threads can read the old value 0, causing a lost update and leaving the final value at count = 1.
Now make the same update atomic:
AtomicInteger count = new AtomicInteger(0);
// Thread A
count.incrementAndGet();
// Thread B
count.incrementAndGet();
The two updates are applied to the same value one after the other:
Thread A Thread B
incrementAndGet() incrementAndGet()
│ │
▼ ▼
0 → 1 1 → 2
The final result is:
count = 2
Here, Atomicity comes from incrementAndGet() itself being an atomic RMW operation. Unlike a Mutex, it does not serialize an entire critical section.
Visibility / Ordering
Now return to the same counter / ready example. With plain reads and writes, observing ready == true alone does not establish the ordering needed to guarantee that the earlier counter = 1 write is visible.
Make only ready atomic:
int counter = 0;
AtomicInteger ready = new AtomicInteger(0);
// Thread A
counter = 1; // A1
ready.set(1); // A2
// Thread B
if (ready.get() == 1) { // B1
System.out.println(counter); // B2
}
If B’s ready.get() observes the value written by A’s ready.set(1), then:
Thread A Thread B
A1 counter = 1
│
│ program order
▼
A2 ready.set(1)
│
│ synchronizes-with
▼
B1 ready.get() == 1
│
│ program order
▼
B2 read counter
By happens-before transitivity:
A1 happens-before A2
A2 happens-before B1
B1 happens-before B2
↓
A1 happens-before B2
So once B observes ready == 1 through the Atomic variable, the earlier counter = 1 write is visible to B, and this result is not allowed:
ready == 1
counter == 0
That is how the same Atomic operation provides Visibility and Ordering in this example.
1.2 Go: sync/atomic
Start with the rules.
atomic.Int64.Add states:
“Add atomically adds delta to x and returns the new value.”
This is the Atomicity guarantee: Add(1) performs the read, increment, and write-back as one atomic update.
For Visibility and Ordering, The Go Memory Model - Atomic Values states:
“If the effect of an atomic operation A is observed by atomic operation B, then A is synchronized before B.”
In other words, if B’s atomic operation observes the result of A’s atomic operation, A is synchronized-before B.
The same section also states that all atomic operations behave as though they were in:
“some sequentially consistent order”
The Go Memory Model defines happens-before as the transitive closure of sequenced-before and synchronized-before.
So atomic operations do more than update values indivisibly. When one atomic operation observes another, they can connect the ordinary memory operations around them into a happens-before chain, providing Visibility and Ordering as well.
Now apply those rules to the same two examples.
Atomicity
A plain count++ can lose an update. Replace it with:
var count atomic.Int64
// Goroutine A
count.Add(1)
// Goroutine B
count.Add(1)
The two updates cannot overwrite one another:
Goroutine A Goroutine B
Add(1) Add(1)
│ │
▼ ▼
0 → 1 1 → 2
The final result is:
count = 2
Here, Atomicity comes directly from the atomic semantics of Add(1).
Visibility / Ordering
Now return to the same counter / ready example, making only ready atomic:
var counter int
var ready atomic.Bool
// Goroutine A
counter = 1 // A1
ready.Store(true) // A2
// Goroutine B
if ready.Load() { // B1
fmt.Println(counter) // B2
}
If B’s ready.Load() observes the value written by A’s ready.Store(true), then:
Goroutine A Goroutine B
A1 counter = 1
│
│ sequenced-before
▼
A2 ready.Store(true)
│
│ synchronized-before
▼
B1 ready.Load() == true
│
│ sequenced-before
▼
B2 read counter
Therefore:
A1 happens-before A2
A2 happens-before B1
B1 happens-before B2
↓
A1 happens-before B2
So once B observes ready == true through the Atomic variable, the earlier counter = 1 write is visible to B, and this result is not allowed:
ready == true
counter == 0
That is how Go Atomics provide Visibility and Ordering in this example.
1.3 CPython: No Symmetric Application-level Atomic API
Python’s standard library currently has no general integer Atomic API symmetric with Java’s AtomicInteger or Go’s atomic.Int64.
Application code therefore has no corresponding set of public Atomic rules to cite. An ordinary counter += 1 is not a language-defined Atomic API. The existence of the GIL in a particular CPython configuration, or the use of _Py_atomic_* inside the Runtime, does not turn it into a cross-implementation guarantee of Atomicity, Visibility, or Ordering for application code.
CPython itself needs Atomics for shared Runtime state. How _Py_atomic_* reaches the compiler and CPU is an implementation question for the next article, not part of Python’s public application-level Atomic semantics.
2. Next: How Are Atomics Implemented?
This article stops at the language level: Java and Go define Atomicity, Visibility, and Ordering through public Atomic APIs and their memory models, while Python application code has no symmetric general-purpose Atomic API.
The next article turns those language-level rules into implementation-level problems by following Java AtomicInteger, Go sync/atomic, and CPython’s internal _Py_atomic_* path from the Runtime down to the CPU.
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub