并发编程(三):互斥锁——语言层的原子性、可见性与有序性
继续使用 counter++,理解互斥锁如何提供原子性、可见性和有序性,并比较 Java synchronized、Go sync.Mutex 与 CPython threading.Lock 的语义差异。

目录
0. 从上一篇继续
这一篇直接从语言内存模型层的规则讨论 Mutex。
1. 三种语言的锁规则分别保证什么?
1.1 Java:synchronized 与 Monitor
Java 的 synchronized 锁定对象关联的 Monitor。JLS §17.1 规定:
“Only one thread at a time may hold a lock on a monitor.”
这条规则对应 Atomicity。同一时刻只有一个线程能持有 Monitor,所以只有它能进入临界区,其他使用同一 Monitor 的线程只能等待。
JLS §17.4.5 还规定:
“An unlock on a monitor happens-before every subsequent lock on that monitor.”
这条规则同时对应 Visibility 和 Ordering。
对于 Visibility,A 的写入发生在 unlock 之前,B 的读取发生在后续 lock 之后。通过 happens-before 的传递性,A 在临界区中的写入对 B 可见。
对于 Ordering,A 内部的写入顺序、unlock → lock 和 B 内部的读取顺序被连成一条 happens-before 链。因此,B 不能看到 ready = true、counter = 0 这种违反该顺序的结果。
还是用上一篇的两个例子来看。
Atomicity
上一篇中,两个线程并发执行:
Thread A Thread B
count++ count++
可能同时读到旧值 0,最终得到 count = 1。
现在把同一个 count++ 放进同一个 Monitor 保护的临界区:
synchronized (lock) {
count++;
}
执行过程会变成:
初始:count = 0
Thread A Thread B
获得 lock
尝试获得 lock
等待
读取 count -> 0
计算 count + 1 -> 1
写回 count -> 1
释放 lock
获得 lock
读取 count -> 1
计算 count + 1 -> 2
写回 count -> 2
释放 lock
最终:
count = 2
这里不是 count++ 这条普通语句本身变成了原子指令,而是同一个 Monitor 把整个临界区串行化了。
Visibility / Ordering
上一篇的另一个例子是:
Thread A Thread B
counter = 1
ready = true read ready == true
│
▼
read counter
现在让两个线程通过同一个 Monitor 访问这组状态:
// Thread A
synchronized (lock) {
counter = 1;
ready = true;
}
// Thread B
synchronized (lock) {
if (ready) {
System.out.println(counter);
}
}
如果 Thread B 在 Thread A 释放 lock 之后获得同一个 lock,就有:
counter = 1
ready = true
│
▼
unlock
│
│ happens-before
▼
lock
│
▼
read ready == true
read counter == 1
因此,当 B 在这次加锁后看到 ready == true 时,它也会看到前面的 counter = 1,不会出现:
ready == true
counter == 0
如果 B 比 A 更早获得锁,它当然可能先看到 ready == false;这并不违反上述规则。
1.2 Go:sync.Mutex
Go 的 sync.Mutex 不绑定某个 Goroutine,也不提供可重入语义。sync.Mutex.Lock 规定:
“If the lock is already in use, the calling goroutine blocks until the mutex is available.”
这条规则对应 Atomicity。一个 Goroutine 持有 Mutex 时,其他 Goroutine 会等待,不能进入受同一 Mutex 保护的临界区。
The Go Memory Model - Locks 还规定:
“For any
sync.Mutexorsync.RWMutexvariableland n < m, call n ofl.Unlock()is synchronized before call m ofl.Lock()returns.”
这条规则同时对应 Visibility 和 Ordering。
对于 Visibility,A 的写入 sequenced-before Unlock,Unlock 又 synchronized-before B 的 Lock 返回,B 的读取发生在 Lock 返回之后。这些关系组成 happens-before,因此 A 的写入对 B 可见。
对于 Ordering,同一条 happens-before 链把 A 内部的写入顺序、Unlock → Lock 和 B 内部的读取顺序连起来。因此,B 同样不能看到 ready = true、counter = 0。
还是同样的两个例子。
Atomicity
把上一篇的 count++ 放进同一个 sync.Mutex 保护的临界区:
mu.Lock()
count++
mu.Unlock()
两个 Goroutine 并发执行时:
初始:count = 0
Goroutine A Goroutine B
Lock()
Lock()
等待
读取 count -> 0
计算 count + 1 -> 1
写回 count -> 1
Unlock()
Lock() 返回
读取 count -> 1
计算 count + 1 -> 2
写回 count -> 2
Unlock()
最终仍然是:
count = 2
同样,原子性来自 Mutex 对临界区的串行化,而不是普通的 count++ 本身。
Visibility / Ordering
再看同一个 counter / ready 例子:
// Goroutine A
mu.Lock()
counter = 1
ready = true
mu.Unlock()
// Goroutine B
mu.Lock()
if ready {
fmt.Println(counter)
}
mu.Unlock()
如果 B 的 Lock() 在 A 的 Unlock() 之后返回:
counter = 1
ready = true
│
▼
Unlock()
│
│ synchronized-before
▼
Lock() returns
│
▼
read ready == true
read counter == 1
因此,当 B 看到 ready == true 时,也会看到 counter == 1,不会出现 ready = true、counter = 0。
1.3 CPython:threading.Lock
Python 官方的 threading.Lock 文档规定:
“A primitive lock is a synchronization primitive that is not owned by a particular thread when locked.”
“All methods are executed atomically.”
第二条规则对应 Atomicity。Lock 已经被获得时,其他线程的 acquire() 会等待;因此同一时刻只有一个线程能进入受同一把 Lock 保护的临界区。
对于 Visibility 和 Ordering,Python 和 Java、Go 不同:Python 没有定义一条正式的 release → acquire happens-before 规则,因此不能像 Java、Go 那样直接从语言级 Memory Model 推导。
同样把上一篇的例子放进 threading.Lock。
Atomicity
with lock:
count += 1
两个线程使用同一把 Lock 时:
初始:count = 0
Thread A Thread B
acquire()
acquire()
等待
读取 count -> 0
计算 count + 1 -> 1
写回 count -> 1
release()
acquire() 返回
读取 count -> 1
计算 count + 1 -> 2
写回 count -> 2
release()
最终:
count = 2
Visibility / Ordering
共享的 counter / ready 也通过同一把 Lock 访问:
# Thread A
with lock:
counter = 1
ready = True
# Thread B
with lock:
if ready:
print(counter)
在 CPython 中,同一把 Lock 会把这两个临界区串行化:A 先写入 counter = 1、ready = True 并释放 Lock,B 再获得同一把 Lock 后读取这组状态。
因此在这个例子里,如果 B 读到 ready == True,对应的 counter 也已经是 1。
至于 CPython 在实现层如何把 release → acquire 落实成跨临界区的可见性和顺序边界,下一篇再继续下钻。
2. 下一篇:互斥锁是怎么实现的?
这一篇停在语言层:从 Atomicity、Visibility 和 Ordering 三个方面,看一把 Mutex 向程序员提供什么保证。
下一篇会把这些语言层保证转换成实现层真正需要解决的问题,分别沿着 Java synchronized、Go sync.Mutex 和 CPython threading.Lock,从 Runtime 一直看到 CPU。
讨论
使用 GitHub 账号参与讨论,评论会保存在 GitHub Issues 中。在 GitHub 查看