并发编程(四):互斥锁的实现——从 Runtime 到 CPU
目录
- 0. 这一篇继续回答什么?
- 1. HotSpot 如何实现 synchronized?
- 2. Go Runtime 如何实现 sync.Mutex?
- 3. CPython 如何实现 threading.Lock?
- 4. 三种实现的共同套路
- 5. 下一篇:Atomic
0. 这一篇继续回答什么?
上一篇从语言内存模型的规则出发,看 Mutex 如何提供 Atomicity、Visibility 和 Ordering。
这一篇继续往下看:Java、Go 和 CPython 分别如何在 Runtime 和 CPU 层实现这些保证。
1. HotSpot 如何实现 synchronized?
1.1 分层
先把 Java 的实现层次固定下来:
Java Language
实例:synchronized
作用:声明同步区域
│
▼
JVM Bytecode
实例:monitorenter / monitorexit(synchronized 代码块)
作用:表达 Monitor 的进入 / 退出
│
▼
JVM Implementation(HotSpot)
实例:Synchronization Runtime
作用:实现 JVM 规定的 Monitor 语义
│
▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Fence
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础
1.2 一次 synchronized 的完整实现路径

上面的时序图只保留跨层主流程。Runtime 内部的竞争路径可以进一步展开为:

1.3 Atomicity
Atomicity 对应上面流程图里的两次原子竞争:
Fast Path:CAS markWord lock bits [Acquire]
Inflated Monitor:CAS _owner [Acquire]
它们都在原子地修改锁状态。多个线程同时竞争时,只有一个线程能够成功获得锁并进入临界区。
1.4 Visibility 和 Ordering
Visibility 和 Ordering 对应流程图里的锁边界:
获取锁:CAS ... [Acquire]
释放锁:release lock state [Release]
Release / Acquire 把前后两个临界区连接起来,使前一个持锁者在临界区中的写入,能够被后一个持锁者按照正确的顺序观察到。
继续向下,这些保证再由 CPU 的 Cache Coherence 和内存顺序约束落实。
所以 Java 与第一篇的硬件模型对应为:
Atomicity
→ Atomic Instruction
→ HotSpot / x86-64: LOCK CMPXCHG
Visibility
→ Cache Coherence
→ 典型 x86-64 CPU:MESI-family(如 MESIF / MOESI)
Ordering
→ Fence
→ HotSpot / x86-64: LOCK ADDL $0, 0(%rsp)
这里的 LOCK ADDL $0, 0(%rsp) 对应 HotSpot 在 Linux x86 上实现 full fence 的实际路径;并不表示每次 synchronized 都会额外执行这条指令。x86 自身的内存顺序规则以及 LOCKed RMW 也会参与建立所需的顺序约束。
相关实现可以继续查看 OpenJDK 的 markWord.hpp、objectMonitor.inline.hpp、graphKit.cpp 和 orderAccess_linux_x86.hpp。
2. Go Runtime 如何实现 sync.Mutex?
2.1 分层
Go API
实例:sync.Mutex / Lock() / Unlock()
作用:声明互斥边界
│
▼
Go Implementation(sync / internal/sync)
实例:Mutex
作用:实现 sync.Mutex 语义
│
▼
Go Runtime / Scheduler
实例:Semaphore
作用:处理 Goroutine 的等待 / 唤醒
│
▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Fence
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础
2.2 一次 sync.Mutex 的完整实现路径

上面的时序图只保留跨层主流程。sync.Mutex 内部的竞争路径可以进一步展开为:

2.3 Atomicity
Atomicity 对应上面流程里的锁状态原子修改:
获取锁:CompareAndSwapInt32(&state, 0, mutexLocked)
释放锁:AddInt32(&state, -mutexLocked)
多个 Goroutine 同时竞争时,只有一个能够成功把 state 从 unlocked 改成 locked,并进入临界区。
在 amd64 上,这两类原子操作分别落到 LOCK CMPXCHGL 和 LOCK XADDL。
2.4 Visibility 和 Ordering
Visibility 和 Ordering 对应 Lock / Unlock 形成的同步边界。前一个持锁者完成 Unlock 后,后一个成功 Lock 的 Goroutine 能按照正确的顺序观察前一个临界区中的写入。
继续向下,这些保证由 CPU 的 Cache Coherence、x86 内存顺序以及原子指令本身的顺序约束落实。
所以 Go 与第一篇的硬件模型对应为:
Atomicity
→ Atomic Instruction
→ Go / x86-64: LOCK CMPXCHGL / LOCK XADDL
Visibility
→ Cache Coherence
→ 典型 x86-64 CPU:MESI-family(如 MESIF / MOESI)
Ordering
→ Fence / 等价顺序约束
→ Go / x86-64: LOCK CMPXCHGL / LOCK XADDL
Go 的这条 amd64 Mutex 路径不需要额外插入一条 MFENCE;LOCKed RMW 本身已经提供了这里需要的原子更新和顺序约束。
实现可以参考 internal/sync/mutex.go、runtime/sema.go 和 internal/runtime/atomic/atomic_amd64.s。
3. CPython 如何实现 threading.Lock?
这一节只讨论当前 CPython。
3.1 分层
Python API
实例:threading.Lock / acquire() / release()
作用:声明互斥边界
│
▼
CPython Binding(_thread)
实例:_thread.lock
作用:将 Python Lock API 映射到 CPython 锁实现
│
▼
CPython Implementation
实例:PyMutex
作用:实现 Lock 语义
│
▼
x86-64 Hardware
实例:Atomic Instruction / Cache Coherence / Fence
作用:提供 Atomicity、Visibility 与 Ordering 的硬件基础
3.2 一次 threading.Lock 的完整实现路径

上面的时序图只保留跨层主流程。CPython 内部的竞争路径可以进一步展开为:

3.3 Atomicity
Atomicity 对应 PyMutex 对 _bits 的原子状态修改:
获取锁:_Py_atomic_compare_exchange_uint8(..., _Py_LOCKED)
释放锁(无 waiter):_Py_atomic_compare_exchange_uint8(..., _Py_UNLOCKED)
存在 waiter 时,释放路径会进入 _PyParkingLot_Unpark(),并由回调原子更新 _bits。
多个线程同时竞争时,只有一个能够成功设置 _Py_LOCKED 并进入临界区。
3.4 Visibility 和 Ordering
Visibility 和 Ordering 对应 acquire() / release() 形成的同步边界。当前 CPython 的默认原子 compare-exchange / store 在 GCC、Clang 实现中使用 __ATOMIC_SEQ_CST。
继续向下,这些保证由 CPU 的 Cache Coherence、x86 内存顺序以及这些顺序一致原子操作落实。
因此当前 CPython 与第一篇的硬件模型对应为:
Atomicity
→ Atomic Instruction
→ CPython / x86-64: LOCK CMPXCHGB
Visibility
→ Cache Coherence
→ 典型 x86-64 CPU:MESI-family(如 MESIF / MOESI)
Ordering
→ Fence / 等价顺序约束
→ CPython / x86-64: SEQ_CST atomic
(典型为 LOCK CMPXCHGB / memory XCHGB)
这里同样不要求每次 Lock 都额外执行 MFENCE;在 x86-64 上,LOCKed RMW 和顺序一致原子操作本身已经提供相应的顺序约束。
实现可以参考 Modules/_threadmodule.c、Python/lock.c、Python/parking_lot.c 和 Include/cpython/pyatomic_gcc.h。
4. 三种实现的共同套路
看完 Java、Go 和 CPython,再把具体实现拿掉,只看一把 Mutex 最终需要解决什么问题。
4.1 谁能进入?——原子修改锁状态
假设一把锁内部只有一个状态:
0 = unlocked
1 = locked
A 和 B 不能先分别读取 0,再同时写入 1。否则两边都会认为自己获得了锁。
因此“检查锁状态并把它改成 locked”必须是一个原子操作,例如 Compare-And-Swap:
Compare-And-Swap(lock_state, 0, 1)
两个执行单元同时竞争:
CPU A CPU B
CAS 0 -> 1 CAS 0 -> 1
│ │
▼ ▼
success failed
只有一个竞争者能够成功修改锁状态。Mutex 再用这个很小的硬件原子操作,保护 counter++ 这样的任意临界区。
4.2 没抢到的人怎么办?——Spin / Park / Wakeup
CAS 失败以后,执行单元不能无休止地竞争锁状态。
常见路径是:
尝试原子获取锁
│
├── 成功 ──> 进入临界区
│
└── 失败
│
├── 短暂 Spin
│ │
│ └── 再次尝试
│
└── Park / 等待
│
└── 锁释放后被唤醒
Spin 适合等待时间很短的情况,可以避免立即进入阻塞路径;竞争持续时则需要 Park,避免一直占用 CPU。
4.3 抢到锁之后,为什么能看到前一个持锁者留下的数据?
前两个问题解决了“谁能拿到锁”和“拿不到锁怎么办”。第三个共同问题是:前一个持锁者在临界区里的写入,为什么能被后一个持锁者看到?
从三种实现的共同模型来看,关键是锁的释放和获取形成了 Release / Acquire 同步边界:
前一个持锁者
临界区写入
│
▼
unlock [Release]
│
▼
lock [Acquire]
│
▼
后一个持锁者
临界区读取
Release 约束释放锁之前的内存操作,Acquire 约束获得锁之后的内存操作。
因此,当后一个执行单元成功获得前一个执行单元释放的同一把锁时,前一个临界区中的写入会排在后一个临界区的读取之前。对 Mutex 来说,这就是三种实现共同需要建立的内存顺序。
具体的 Runtime 和 CPU 如何落实这个 Release / Acquire 边界,前三节已经分别展开。
5. 下一篇:Atomic
Mutex 的巧妙之处在于:它只需要维护一个很小的原子状态,就能为任意大小的临界区建立互斥边界。
Atomic 则更进一步收缩了保护范围:它不再保护一整段临界区,而是直接保证一个共享变量的一次读、写或更新不可分割。
下一篇继续看这个更小的同步单元是如何实现的。
本文首发于 ThinkerQAQ 的个人博客,由作者本人同步发布。原文可能持续修订,最新版本请以个人博客为准。

浙公网安备 33010602011771号