并发编程(二):语言内存模型——程序员可以依赖的规则

从硬件内存模型回到语言层,理解语言为什么需要定义内存模型,以及 Java、Go 与 CPython 分别为并发程序提供哪些基本保证。

中文发布于 更新于 2026/09/10

目录


0. 从上一篇继续

上一篇讲的是硬件层提供的能力,

但程序员写的是:

Java    -> synchronized / volatile / AtomicInteger
Go      -> sync.Mutex / sync/atomic / channel
Python  -> threading.Lock / queue.Queue

而不是某一种 CPU 的机器指令。

所以这一篇只回答一个问题:

面对不同的底层硬件,编程语言向程序员提供哪些统一的并发规则?


1. 为什么语言还需要自己的并发语义?

源代码需要经过编译器和 Runtime,最终才能在 CPU 上执行。编译器会优化代码,x86、ARM 等处理器允许的内存访问顺序也不完全相同。

如果程序员必须分别研究每一种编译器、Runtime 和 CPU,跨平台并发编程就很难成立。

因此还需要一层语言规则:

Concurrency Tools / Synchronization Mechanisms
    └─ Mutex / Atomic / volatile / Channel / Queue
            ↓
Language Memory Model / Concurrency Semantics
    ├─ JMM / Go Memory Model / CPython Concurrency Semantics
    └─  HotSpot JIT / Go Compiler + Runtime / CPython Runtime 
            ↓
Hardware
    ├─ Computer ArchitectureVon Neumann Architecture / CPU / Memory
    ├─ Hardware Memory Modelx86-TSO / ARM Memory Model
    ├─ Instruction / CPU PrimitiveLOAD / STORE / Atomic RMW / Fence
    └─ MicroarchitectureCache / Store Buffer / Cache Coherence / Out-of-Order Execution

语言内存模型定义程序员可以依赖的行为,编译器和 Runtime 再针对具体硬件实现这些保证。

例如,程序员需要判断的是:

当 Thread B 看到 ready = true 时,语言是否保证它也能看到之前写入的 count = 1

至于在 x86 或 ARM 上需要生成哪些指令,不应该由业务代码逐一判断。


2. 语言内存模型需要回答什么?

继续使用前面的两个例子:

counter++

以及:

counter = 1
ready = true

语言层需要回答:

问题 回到例子意味着什么
Atomicity counter++ 是否可以被视为不可分割?如果不可以,哪些工具能够提供原子性?
Visibility A 写入 counter = 1 后,B 在什么条件下能够可靠地看到它?
Ordering 当 B 看到 ready = true 时,是否也必须看到前面的 counter = 1

语言内存模型不要求程序员直接分析 Cache、Store Buffer 或 Fence。它定义源代码层允许出现哪些结果;底层实现负责满足这些规则。


2.1 Java Memory Model

Java Memory Model(JMM)定义 Java 线程之间允许出现哪些内存访问结果,以及同步操作能够建立什么关系。

仍然看:

int count = 0;

// Thread A
count = 1;

// Thread B
System.out.println(count);

Thread B 不保证看到 1,因为两个线程之间没有建立 JMM 规定的同步关系。

JMM 使用 happens-before 描述程序员可以依赖的顺序。JLS §17.4.5 规定:如果一个操作 happens-before 另一个操作,那么前者的结果对后者可见,而且前者在后者之前有序。

JMM 同时规定原子性的边界。count++ 是复合操作,本身不具备临界区原子性;需要使用 synchronizedAtomic 类型。

因此,JMM 并不只包含 happens-before。它还需要定义读取允许观察到哪些写入、同步顺序、原子操作以及数据竞争程序的行为。

具体工具会建立不同的关系,例如:

synchronized
volatile
Thread.start()
Thread.join()
Atomic

后面的文章再逐个展开。


2.2 Go Memory Model

The Go Memory Model 也需要回答一个读取在什么条件下能够观察到另一个 Goroutine 的写入。

var count int

go func() {
    count = 1
}()

go func() {
    fmt.Println(count)
}()

这里存在 Data Race,第二个 Goroutine 不能依赖第一个 Goroutine 的写入结果。

Go 使用三种关系描述顺序:

sequenced-before
synchronized-before
happens-before

同一个 Goroutine 内部的操作形成 sequenced-before;Mutex、Channel、Atomic 等同步操作可以建立 synchronized-before;两者共同构成 happens-before

Go 也区分普通操作和原子操作。counter++ 不是一个原子更新,需要使用 sync.Mutexsync/atomic

Java 和 Go 都使用 happens-before,但具体规则不能直接互换。分析代码时仍要回到各自的 Memory Model。


2.3 CPython 的并发语义

Python 没有一套适用于所有解释器实现、与 JMM 或 Go Memory Model 同等级的统一并发内存模型。本系列讨论最常用的 CPython。

2.3.1 GIL 模式

默认的 GIL-enabled CPython 中:

线 GIL Python 

但有 GIL 不等于共享状态天然线程安全。

解释器仍会在线程之间切换;执行阻塞 I/O 时会释放 GIL,扩展模块也可以主动释放它。GIL 主要保护 CPython 解释器及其对象模型,不能代替应用程序自己的同步。

因此:

counter += 1

在 GIL 模式下,也不是并发安全的

2.3.2 Free-threaded 模式

从 Python 3.13 开始,CPython 提供可禁用 GIL 的 Free-threaded 构建。在这种模式下,多个线程可以同时在不同 CPU Core 上执行 Python 代码。

GIL-enabled CPython Free-threaded CPython
Python 代码执行 同一时刻只有一个线程持有 GIL 多个线程可以在不同 Core 上并行执行
解释器内部状态 主要由 GIL 保护 使用更细粒度的锁和原子操作保护
应用层共享状态 需要显式同步 同样需要显式同步

Free-threaded 模式加强的是解释器内部并行能力,不会自动把应用层的 counter += 1 变成线程安全操作。部分尚未适配的扩展模块还可能在导入时重新启用 GIL。

具体范围和限制可以参考 Python 官方的 Free-threading 文档PEP 703

2.3.3 程序应该依赖什么?

Python 程序应该依赖明确的同步 API:

threading.Lock
queue.Queue
threading.Event
threading.Condition

曾经提出统一并发内存模型的 PEP 583 已经撤回。因此,不应该把 CPython Lock 的实现行为直接称为 Python 官方的 happens-before 规则。

在 Python 中讨论并发安全,需要先说明使用的是哪一种同步 API;继续下钻实现时,还要说明讨论的是 CPython、PyPy,还是其他解释器。


3. 下一篇:互斥锁

下一篇开始讨论第一个具体同步工具:Mutex。

先回答:

 Atomicity


JavaGo  CPython 

再下一篇沿着三种语言的具体实现,从 Runtime 继续追到 CPU。

讨论

使用 GitHub 账号参与讨论,评论会保存在 GitHub Issues 中。在 GitHub 查看