Concurrency Programming (3): Mutexes — Atomicity, Visibility, and Ordering at the Language Level
Continues with counter++ to explain how mutexes provide atomicity, visibility, and ordering, then compares Java synchronized, Go sync.Mutex, and CPython threading.Lock.

Table of Contents
- 0. Continue from the Previous Article
- 1. What Guarantees Do the Lock Rules in Each Language Provide?
- 2. Next: How Is a Mutex Implemented?
0. Continue from the Previous Article
This article discusses Mutexes directly from the rules defined at the language memory model layer.
1. What Guarantees Do the Lock Rules in Each Language Provide?
1.1 Java: synchronized and Monitors
Java’s synchronized locks the monitor associated with an object. JLS §17.1 states:
“Only one thread at a time may hold a lock on a monitor.”
This rule provides the exclusion needed for atomicity. At most one thread can hold the monitor, so only that thread may execute the protected critical section while competitors wait.
JLS §17.4.5 also states:
“An unlock on a monitor happens-before every subsequent lock on that monitor.”
This rule provides both visibility and ordering.
For visibility, A’s writes are before its unlock, while B’s reads are after the subsequent lock. Through happens-before transitivity, the writes in A’s critical section become visible to B.
For ordering, A’s program order, the unlock → lock relationship, and B’s program order form one happens-before chain. B therefore cannot observe a result such as ready = true, counter = 0 when that would violate the established ordering.
Now reuse the same two examples from the previous article.
Atomicity
Previously, two threads executing:
Thread A Thread B
count++ count++
could both read the old value 0 and leave the final value at count = 1.
Now place the same count++ inside a critical section guarded by the same monitor:
synchronized (lock) {
count++;
}
The execution is serialized:
initial: count = 0
Thread A Thread B
acquire lock
try to acquire lock
wait
read count -> 0
compute count + 1 -> 1
write count -> 1
release lock
acquire lock
read count -> 1
compute count + 1 -> 2
write count -> 2
release lock
The final result is:
count = 2
The ordinary count++ statement has not become an atomic CPU instruction. The monitor makes the entire critical section execute with mutual exclusion relative to other code using the same monitor.
Visibility / Ordering
The other example from the previous article was:
Thread A Thread B
counter = 1
ready = true read ready == true
│
▼
read counter
Now access that state through the same monitor:
// Thread A
synchronized (lock) {
counter = 1;
ready = true;
}
// Thread B
synchronized (lock) {
if (ready) {
System.out.println(counter);
}
}
If Thread B acquires lock after Thread A releases it:
counter = 1
ready = true
│
▼
unlock
│
│ happens-before
▼
lock
│
▼
read ready == true
read counter == 1
So once B observes ready == true after that lock acquisition, it also observes the earlier counter = 1; it cannot observe:
ready == true
counter == 0
If B acquires the lock before A, it may of course observe ready == false; that does not violate the rule.
1.2 Go: sync.Mutex
Go’s sync.Mutex is not owned by a particular goroutine and is not reentrant. sync.Mutex.Lock states:
“If the lock is already in use, the calling goroutine blocks until the mutex is available.”
This gives the exclusion needed for atomicity. While one goroutine holds the mutex, other goroutines attempting to acquire the same mutex wait rather than entering the critical section.
The Go Memory Model - Locks further states:
“For any
sync.Mutexorsync.RWMutexvariableland n < m, call n ofl.Unlock()is synchronized before call m ofl.Lock()returns.”
This rule provides visibility and ordering.
For visibility, A’s writes are sequenced-before Unlock; Unlock is synchronized-before B’s Lock returns; B’s reads are sequenced after that return. Together these relationships form happens-before, so A’s writes are visible to B.
For ordering, the same happens-before chain connects A’s writes, Unlock → Lock, and B’s reads. B likewise cannot observe ready = true, counter = 0 when the synchronization relationship requires the earlier counter write to be visible.
Use the same two examples again.
Atomicity
Place the previous count++ inside a critical section guarded by the same sync.Mutex:
mu.Lock()
count++
mu.Unlock()
When two goroutines execute it concurrently:
initial: count = 0
Goroutine A Goroutine B
Lock()
Lock()
wait
read count -> 0
compute count + 1 -> 1
write count -> 1
Unlock()
Lock() returns
read count -> 1
compute count + 1 -> 2
write count -> 2
Unlock()
The final result is:
count = 2
Again, the atomicity comes from serializing the critical section with the mutex, not from ordinary count++ itself.
Visibility / Ordering
Now reuse the same counter / ready example:
// Goroutine A
mu.Lock()
counter = 1
ready = true
mu.Unlock()
// Goroutine B
mu.Lock()
if ready {
fmt.Println(counter)
}
mu.Unlock()
If B’s Lock() returns after A’s Unlock():
counter = 1
ready = true
│
▼
Unlock()
│
│ synchronized-before
▼
Lock() returns
│
▼
read ready == true
read counter == 1
So if B observes ready == true, it also observes counter == 1; the result ready = true, counter = 0 is not allowed by this synchronization chain.
1.3 CPython: threading.Lock
Python’s official threading.Lock documentation states:
“A primitive lock is a synchronization primitive that is not owned by a particular thread when locked.”
“All methods are executed atomically.”
The locking semantics provide mutual exclusion: when the lock is already acquired, another thread’s acquire() waits until the lock becomes available, so the same lock serializes entry into the protected critical section.
For Visibility and Ordering, Python differs from Java and Go: it does not define a formal language-level release → acquire happens-before rule, so these guarantees cannot be derived from a language memory model in the same way.
Now put the same examples behind threading.Lock.
Atomicity
with lock:
count += 1
When two threads use the same Lock:
initial: count = 0
Thread A Thread B
acquire()
acquire()
wait
read count -> 0
compute count + 1 -> 1
write count -> 1
release()
acquire() returns
read count -> 1
compute count + 1 -> 2
write count -> 2
release()
The final result is:
count = 2
Visibility / Ordering
Protect the shared counter / ready state with the same Lock as well:
# Thread A
with lock:
counter = 1
ready = True
# Thread B
with lock:
if ready:
print(counter)
In CPython, the same Lock serializes these two critical sections: A writes counter = 1 and ready = True, releases the Lock, and B then acquires that same Lock before reading the shared state.
So in this example, if B reads ready == True, the corresponding counter value is already 1.
How CPython implements release → acquire as the visibility and ordering boundary between these critical sections is an implementation detail for the next article.
2. Next: How Is a Mutex Implemented?
This article stops at the language level: it looks at what guarantees a Mutex provides to programmers from the perspectives of Atomicity, Visibility, and Ordering.
The next article turns those language-level guarantees into implementation-level problems, following Java synchronized, Go sync.Mutex, and CPython threading.Lock from the Runtime down to the CPU.
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub