Table of Contents
- 0. What Does This Article Continue to Answer?
- 1. How Does HotSpot Implement synchronized?
- 2. How Does the Go Runtime Implement sync.Mutex?
- 3. How Does CPython Implement threading.Lock?
- 4. The Common Pattern Across the Three Implementations
- 5. Next: Atomic
0. What Does This Article Continue to Answer?
The previous article started from the rules of the language memory model and looked at how a Mutex provides Atomicity, Visibility, and Ordering.
This article continues downward: how Java, Go, and CPython implement those guarantees at the Runtime and CPU layers.
1. How Does HotSpot Implement synchronized?
1.1 Layers
First, fix the implementation layers for Java:
Java Language
Example: synchronized
Role: declare a synchronized region
│
▼
JVM Bytecode
Example: monitorenter / monitorexit (synchronized blocks)
Role: express entering / exiting a Monitor
│
▼
JVM Implementation (HotSpot)
Example: Synchronization Runtime
Role: implement the Monitor semantics required by the JVM
│
▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware foundation for Atomicity, Visibility, and Ordering
1.2 The Complete Path of One synchronized Operation
The sequence diagram above keeps only the main cross-layer path. The contention path inside the Runtime can be expanded further:
1.3 Atomicity
Atomicity corresponds to the two atomic competitions in the flowchart above:
Fast Path: CAS markWord lock bits [Acquire]
Inflated Monitor: CAS _owner [Acquire]
Both atomically modify lock state. When multiple threads compete at the same time, only one thread can successfully acquire the lock and enter the critical section.
1.4 Visibility and Ordering
Visibility and Ordering correspond to the lock boundaries in the flowchart:
Acquire lock: CAS ... [Acquire]
Release lock: release lock state [Release]
Release / Acquire connects the two critical sections so that writes from the previous lock holder can be observed by the next lock holder in the correct order.
Going further down, these guarantees are implemented through CPU Cache Coherence and memory-ordering constraints.
So Java maps back to the hardware model from the first article as follows:
Atomicity
→ Atomic Instruction
→ HotSpot / x86-64: LOCK CMPXCHG
Visibility
→ Cache Coherence
→ Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)
Ordering
→ Fence
→ HotSpot / x86-64: LOCK ADDL $0, 0(%rsp)
LOCK ADDL $0, 0(%rsp) is the actual full-fence path used by HotSpot on Linux x86. This does not mean every synchronized operation executes an extra copy of that instruction; x86 ordering rules and LOCKed RMW operations also participate in establishing the required ordering.
For implementation details, see OpenJDK's markWord.hpp, objectMonitor.inline.hpp, graphKit.cpp, and orderAccess_linux_x86.hpp.
2. How Does the Go Runtime Implement sync.Mutex?
2.1 Layers
Go API
Example: sync.Mutex / Lock() / Unlock()
Role: declare a mutual-exclusion boundary
│
▼
Go Implementation (sync / internal/sync)
Example: Mutex
Role: implement sync.Mutex semantics
│
▼
Go Runtime / Scheduler
Example: Semaphore
Role: handle Goroutine waiting / wakeup
│
▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware foundation for Atomicity, Visibility, and Ordering
2.2 The Complete Path of One sync.Mutex Operation
The sequence diagram above keeps only the main cross-layer path. The contention path inside sync.Mutex can be expanded further:
2.3 Atomicity
Atomicity corresponds to the atomic lock-state modifications in the flow above:
Acquire lock: CompareAndSwapInt32(&state, 0, mutexLocked)
Release lock: AddInt32(&state, -mutexLocked)
When multiple Goroutines compete at the same time, only one can successfully change state from unlocked to locked and enter the critical section.
On amd64, these two classes of atomic operation map to LOCK CMPXCHGL and LOCK XADDL.
2.4 Visibility and Ordering
Visibility and Ordering correspond to the synchronization boundary formed by Lock / Unlock. After one holder completes Unlock, a later Goroutine that successfully executes Lock can observe writes from the previous critical section in the required order.
Going further down, these guarantees are implemented through CPU Cache Coherence, x86 memory ordering, and the ordering constraints of the atomic instructions themselves.
So Go maps back to the hardware model from the first article as follows:
Atomicity
→ Atomic Instruction
→ Go / x86-64: LOCK CMPXCHGL / LOCK XADDL
Visibility
→ Cache Coherence
→ Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)
Ordering
→ Fence / equivalent ordering constraint
→ Go / x86-64: LOCK CMPXCHGL / LOCK XADDL
This amd64 Mutex path does not need an additional MFENCE; the LOCKed RMW operations already provide the atomic update and ordering constraints required here.
For implementation details, see internal/sync/mutex.go, runtime/sema.go, and internal/runtime/atomic/atomic_amd64.s.
3. How Does CPython Implement threading.Lock?
This section discusses current CPython only.
3.1 Layers
Python API
Example: threading.Lock / acquire() / release()
Role: declare a mutual-exclusion boundary
│
▼
CPython Binding (_thread)
Example: _thread.lock
Role: map the Python Lock API to CPython's lock implementation
│
▼
CPython Implementation
Example: PyMutex
Role: implement Lock semantics
│
▼
x86-64 Hardware
Example: Atomic Instruction / Cache Coherence / Fence
Role: provide the hardware foundation for Atomicity, Visibility, and Ordering
3.2 The Complete Path of One threading.Lock Operation
The sequence diagram above keeps only the main cross-layer path. CPython's internal contention path can be expanded further:
3.3 Atomicity
Atomicity corresponds to the atomic state changes that PyMutex performs on _bits:
Acquire lock: _Py_atomic_compare_exchange_uint8(..., _Py_LOCKED)
Release lock (no waiter): _Py_atomic_compare_exchange_uint8(..., _Py_UNLOCKED)
When waiters exist, the release path enters _PyParkingLot_Unpark(), whose callback atomically updates _bits.
When multiple threads compete at the same time, only one can successfully set _Py_LOCKED and enter the critical section.
3.4 Visibility and Ordering
Visibility and Ordering correspond to the synchronization boundary formed by acquire() / release(). In current CPython, the default atomic compare-exchange and store operations use __ATOMIC_SEQ_CST in the GCC / Clang implementation.
Going further down, these guarantees are implemented through CPU Cache Coherence, x86 memory ordering, and those sequentially consistent atomic operations.
So current CPython maps back to the hardware model from the first article as follows:
Atomicity
→ Atomic Instruction
→ CPython / x86-64: LOCK CMPXCHGB
Visibility
→ Cache Coherence
→ Typical x86-64 CPUs: MESI-family (e.g. MESIF / MOESI)
Ordering
→ Fence / equivalent ordering constraint
→ CPython / x86-64: SEQ_CST atomic
(typically LOCK CMPXCHGB / memory XCHGB)
This likewise does not require every Lock operation to execute an additional MFENCE; on x86-64, LOCKed RMW and sequentially consistent atomic operations themselves provide the corresponding ordering constraints.
For implementation details, see Modules/_threadmodule.c, Python/lock.c, Python/parking_lot.c, and Include/cpython/pyatomic_gcc.h.
4. The Common Pattern Across the Three Implementations
After looking at Java, Go, and CPython, remove the implementation-specific details and focus only on the problems that a Mutex ultimately needs to solve.
4.1 Who Gets In? — Atomically Modify Lock State
Suppose a lock has only one internal state:
0 = unlocked
1 = locked
A and B must not both read 0 first and then both write 1. Otherwise, both would believe they acquired the lock.
Therefore, "check the lock state and change it to locked" must be an atomic operation, such as Compare-And-Swap:
Compare-And-Swap(lock_state, 0, 1)
When two execution units compete:
CPU A CPU B
CAS 0 -> 1 CAS 0 -> 1
│ │
▼ ▼
success failed
Only one competitor can successfully modify the lock state. A Mutex then uses this small hardware atomic operation to protect an arbitrary critical section such as counter++.
4.2 What Happens If You Don't Get the Lock? — Spin / Park / Wakeup
After CAS fails, an execution unit cannot compete for the lock state forever.
A common path is:
try atomic lock acquisition
│
├── success ──> enter critical section
│
└── failure
│
├── Spin briefly
│ │
│ └── retry
│
└── Park / wait
│
└── wake after lock release
Spin is suitable when the wait is expected to be very short because it avoids immediately entering a blocking path. If contention persists, Park avoids continuously occupying the CPU.
4.3 After Acquiring the Lock, Why Can You See the Data Left by the Previous Holder?
The first two questions answer "who gets the lock?" and "what happens if you don't get it?" The third common question is: why can writes performed by the previous lock holder inside the critical section be seen by the next lock holder?
Across the three implementations, the key common model is that lock release and lock acquisition form a Release / Acquire synchronization boundary:
previous lock holder
critical-section writes
│
▼
unlock [Release]
│
▼
lock [Acquire]
│
▼
next lock holder
critical-section reads
Release constrains memory operations before the lock is released, while Acquire constrains memory operations after the lock is acquired.
Therefore, when the next execution unit successfully acquires the same lock released by the previous execution unit, writes in the earlier critical section are ordered before reads in the later critical section. For a Mutex, this is the memory ordering that all three implementations need to establish.
The first three sections already showed how each Runtime and CPU implementation realizes this Release / Acquire boundary.
5. Next: Atomic
The cleverness of a Mutex is that it only needs to maintain a very small atomic state to establish a mutual-exclusion boundary around a critical section of arbitrary size.
Atomic narrows the protected scope further: instead of protecting an entire critical section, it directly guarantees that a single read, write, or update of a shared variable is indivisible.
The next article continues with how this smaller synchronization unit is implemented.
This article was first published on ThinkerQAQ's personal blog and syndicated here by the author. The original article may be revised over time; please refer to the personal blog for the latest version.






Top comments (1)