Concurrency Programming (7): volatile — Visibility and Ordering for ready and counter
Uses the counter + ready example to explain the visibility and ordering guarantees of Java volatile, and why Go and Python do not have an equivalent volatile keyword.
Table of Contents
- 0. From Atomic to volatile
- 1. What Do We Need to Guarantee This Time?
- 2. Java: volatile
- 3. Go Has No volatile Keyword
- 4. CPython Has No volatile Keyword
- 5. Comparing the Three Languages
- 6. When Is This Pattern Appropriate?
- 7. Next: From Mutex to Read-Write Locks
0. From Atomic to volatile
The previous two articles centered on counter++: Atomic operations make a Read-Modify-Write update indivisible, and the runtime maps those semantics onto CPU atomic instructions and memory ordering.
But not every scenario needs an atomic update. Return to the counter + ready example:
counter = 0
ready = false
Thread A:
counter = 1
ready = true
Thread B:
if ready {
print(counter)
}
There are not multiple threads jointly updating counter. We only want this guarantee:
If Thread B has observed
ready = true, then its subsequent read ofcountermust return1, not the stale value0.
1. What Do We Need to Guarantee This Time?
Write out the order within the two threads:
Thread A Thread B
counter = 1 read ready
│ │
▼ ▼
ready = true read counter
The program needs to establish this relationship:
A writes counter
↓
A writes ready
↓ synchronization relationship
B reads ready
↓
B reads counter
Without a synchronization relationship in the middle, observing ready = true is not enough by source-code order alone to conclude that Thread B must also observe counter = 1.
So this example mainly needs two guarantees:
| Problem | Required guarantee |
|---|---|
Can B observe A’s counter = 1? |
Visibility |
Can the observable effect of counter = 1 appear after ready = true? |
Ordering |
This scenario does not require an atomic RMW such as counter++, so for now the focus is visibility and ordering rather than atomicity.
2. Java: volatile
Java can declare ready as volatile:
int counter = 0;
volatile boolean ready = false;
// Thread A
counter = 1;
ready = true;
// Thread B
if (ready) {
System.out.println(counter);
}
Only ready is volatile. counter remains an ordinary variable.
That is enough because Thread B uses ready to determine whether Thread A has finished publishing the state of counter.
2.1 How Is happens-before Established?
JLS §17.4.4 defines a synchronizes-with relationship from a write to a volatile variable to a subsequent read of that same variable by another thread.
Applied to this example:
Thread A Thread B
counter = 1
│
│ program order
▼
volatile ready = true
│
└── synchronizes-with ─────────► read volatile ready == true
│
│ program order
▼
read counter
Through happens-before transitivity:
A writes counter
happens-before
B reads counter
Therefore, once B reads the ready = true value written by A, B’s following read of counter must observe 1.
That is the useful way to understand volatile here. It is not best modeled as “always force a read from main memory.” The JMM constrains which program outcomes are observable.
2.2 volatile Does Not Guarantee Atomicity
Consider:
volatile int counter = 0;
counter++;
Even though counter is volatile, counter++ still contains a compound operation conceptually equivalent to:
LOAD
ADD
STORE
volatile does not turn those steps into one indivisible RMW. If multiple threads concurrently execute counter++, use AtomicInteger or a mutex instead.
2.3 synchronized, Atomic, and volatile
Atomic is not a Java keyword; this table uses common AtomicInteger methods as the representative Atomic API:
synchronized |
AtomicInteger |
volatile |
|
|---|---|---|---|
| Atomicity | Entire protected critical section cannot interleave | Single-variable RMW methods such as incrementAndGet() are atomic |
Does not provide compound-operation atomicity |
| Visibility | Writes before unlock are visible after a subsequent lock on the same monitor | Common get(), set(), and RMW methods provide their documented memory effects |
A volatile write is visible to a subsequent read of that volatile variable under the JMM rule |
| Ordering | Unlock and subsequent lock establish ordering | Common methods establish ordering according to their specified memory effects | Volatile write and subsequent read establish the relevant ordering |
3. Go Has No volatile Keyword
Go has no keyword corresponding to Java volatile. sync/atomic provides Atomic APIs; it is not simply “Go’s version of volatile.”
When synchronization is required in Go, use the synchronization operation whose semantics match the problem—such as atomics, mutexes, or channels—rather than looking for a volatile field modifier.
4. CPython Has No volatile Keyword
Python also has no keyword corresponding to Java volatile. threading.Event is a thread-communication API, not a volatile field, and the GIL is not a replacement for a volatile synchronization relationship.
Use explicit synchronization APIs whose behavior is documented for the communication pattern you need.
5. Comparing the Three Languages
| Java | Go | CPython | |
|---|---|---|---|
volatile keyword |
Yes | No | No |
The absence of a keyword does not mean Go or Python cannot synchronize publication. It means their public concurrency abstractions express the synchronization boundary differently.
6. When Is This Pattern Appropriate?
This pattern fits a one-way publication shape:
write a group of state
↓
publish one flag last
↓
another execution unit observes the flag and then reads the state
Examples include:
- initialization-complete flags;
- configuration-loaded flags;
- background-task stop requests;
- safe publication of an immutable object.
It is not enough when:
- multiple threads concurrently execute
counter++; - several fields must change as one indivisible transaction;
- code checks state and then conditionally modifies it;
- contenders need queuing or blocking around a critical section.
Those problems call for Atomic RMW, CAS, or a mutex rather than merely applying volatile to one field.
7. Next: From Mutex to Read-Write Locks
A mutex permits only one thread in the critical section at a time. Even when several threads only read data, they still wait for each other.
The next article continues with shared memory and asks how a read-write lock allows multiple readers to proceed together, comparing Java ReentrantReadWriteLock, Go sync.RWMutex, and the corresponding situation in CPython.
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub