并发编程(五):Atomic——语言层的原子性、可见性与有序性

继续使用 counter 与 ready,从语言内存模型和公开 API 规则理解 Java AtomicInteger、Go sync/atomic 与 CPython 的 Atomic 语义边界。

中文发布于 更新于 2026/10/03约 3 分钟读完
并发编程(五):Atomic——语言层的原子性、可见性与有序性

目录


0. 从上一篇继续

这一篇直接从语言内存模型层的规则讨论 Atomic。


1. 三种语言的 Atomic 规则分别保证什么?

1.1 Java:AtomicInteger

先看规则。

AtomicInteger.incrementAndGet() 原文:

“Atomically increments the current value, with memory effects as specified by VarHandle.getAndAdd.”

这条规则对应 Atomicity。读取旧值、加一、写回作为一次不可分割的 Atomic RMW 完成。

对于 Visibility 和 Ordering,AtomicInteger 的普通读写具有 volatile 语义:get() 对应 VarHandle.getVolatile,set() 对应 VarHandle.setVolatile。

JLS §17.4.4 Synchronization Order 对 volatile 的规则是:

“A write to a volatile variable v synchronizes-with all subsequent reads of v by any thread.”

因此,一个线程对 Atomic 变量的 volatile 写,可以与另一个线程后续观察到该写入的 volatile 读建立 synchronizes-with 关系。

JLS §17.4.5 Happens-before Order 还规定:

“If one action happens-before another, then the first is visible to and ordered before the second.”

这条 happens-before 关系同时给出 Visibility 和 Ordering:前面的写入不仅对后面的读取可见,而且必须按照 happens-before 规定的顺序被观察。

还是用前面的两个例子来看。

Atomicity

前面的普通版本:

Thread A                    Thread B

count++                     count++

可能因为两个线程都读到旧值 0,最终得到 count = 1。

现在把同一个更新改成 Atomic:

AtomicInteger count = new AtomicInteger(0);

// Thread A
count.incrementAndGet();

// Thread B
count.incrementAndGet();

两次更新会依次作用在同一个值上:

Thread A                    Thread B

incrementAndGet()           incrementAndGet()
        │                           │
        ▼                           ▼
      0 → 1                       1 → 2

最终:

count = 2

这里 Atomicity 来自 incrementAndGet() 本身就是一次原子 RMW,而不是像 Mutex 那样把整个临界区串行化。

Visibility / Ordering

再看同一个 counter / ready 例子。普通变量版本的问题是:B 即使看到 ready == true,也不能仅靠普通读写推出前面的 counter = 1 已经对它可见。

现在只把 ready 改成 Atomic:

int counter = 0;
AtomicInteger ready = new AtomicInteger(0);

// Thread A
counter = 1;        // A1
ready.set(1);       // A2

// Thread B
if (ready.get() == 1) {     // B1
    System.out.println(counter); // B2
}

如果 B 的 ready.get() 观察到 A 的 ready.set(1),那么:

Thread A                              Thread B

A1  counter = 1
        │
        │ program order
        ▼
A2  ready.set(1)
        │
        │ synchronizes-with
        ▼
                                    B1  ready.get() == 1
                                        │
                                        │ program order
                                        ▼
                                    B2  read counter

通过 happens-before 的传递性:

A1 happens-before A2
A2 happens-before B1
B1 happens-before B2

        ↓

A1 happens-before B2

所以当 B 已经通过 Atomic ready 观察到 ready == 1 时,前面的 counter = 1 对 B 可见,并且不能再出现:

ready == 1
counter == 0

这就是 Atomic 在这个例子里提供的 Visibility 和 Ordering。

1.2 Go:sync/atomic

先看规则。

atomic.Int64.Add 原文:

“Add atomically adds delta to x and returns the new value.”

这条规则对应 Atomicity:Add(1) 把读取、加一和写回作为一次原子更新。

对于 Visibility 和 Ordering,The Go Memory Model - Atomic Values 规定:

“If the effect of an atomic operation A is observed by atomic operation B, then A is synchronized before B.”

也就是说,如果 B 的 Atomic 操作观察到了 A 的 Atomic 更新,那么 A 和 B 之间建立 synchronized-before。

同一节还规定,所有 Atomic 操作表现得像处在:

“some sequentially consistent order”

而 Go Memory Model 把 happens-before 定义为 sequenced-before 和 synchronized-before 的传递闭包。

因此,Atomic 操作不仅提供原子更新;当一个 Atomic 操作观察到另一个 Atomic 操作的结果时,也可以把前后的普通内存操作串成 happens-before 链,从而建立 Visibility 和 Ordering。

还是用同样的两个例子。

Atomicity

普通的 count++ 可能发生丢失更新。现在改成:

var count atomic.Int64

// Goroutine A
count.Add(1)

// Goroutine B
count.Add(1)

两次更新不会丢掉其中一次:

Goroutine A                 Goroutine B

Add(1)                      Add(1)
  │                           │
  ▼                           ▼
0 → 1                       1 → 2

最终:

count = 2

这里 Atomicity 来自 Add(1) 本身的原子更新语义。

Visibility / Ordering

再看同一个 counter / ready 例子,只把 ready 改成 Atomic:

var counter int
var ready atomic.Bool

// Goroutine A
counter = 1          // A1
ready.Store(true)    // A2

// Goroutine B
if ready.Load() {    // B1
    fmt.Println(counter) // B2
}

如果 B 的 ready.Load() 观察到 A 的 ready.Store(true),那么:

Goroutine A                           Goroutine B

A1  counter = 1
        │
        │ sequenced-before
        ▼
A2  ready.Store(true)
        │
        │ synchronized-before
        ▼
                                    B1  ready.Load() == true
                                        │
                                        │ sequenced-before
                                        ▼
                                    B2  read counter

于是:

A1 happens-before A2
A2 happens-before B1
B1 happens-before B2

        ↓

A1 happens-before B2

所以当 B 已经通过 Atomic ready 观察到 ready == true 时,前面的 counter = 1 对 B 可见,并且不能再出现:

ready == true
counter == 0

这就是 Go Atomic 在这个例子里提供的 Visibility 和 Ordering。

1.3 CPython:应用层没有对称的 Atomic API

Python 标准库目前没有与 Java AtomicInteger、Go atomic.Int64 对称的通用整数 Atomic API。

因此,Python 应用代码没有一组可以像 Java、Go 那样直接引用的 Atomic 规则。普通的 counter += 1 不是 Python 语言层定义的 Atomic API;也不能因为某个 CPython 配置存在 GIL,或者 Runtime 内部使用 _Py_atomic_*,就把它当作应用层可以依赖的跨实现 Atomicity、Visibility 或 Ordering 保证。

CPython Runtime 确实需要 Atomic 来同步自己的共享状态。它如何把 _Py_atomic_* 落到编译器和 CPU,是下一篇的实现问题,而不是 Python 应用层公开语义的一部分。


2. 下一篇:Atomic 是怎么实现的?

这一篇停在语言层:Java 和 Go 分别通过公开 Atomic API 与内存模型定义 Atomicity、Visibility 和 Ordering;Python 应用层则没有对称的通用 Atomic API。

下一篇会把这些语言层规则转换成实现层真正需要解决的问题,分别沿着 Java AtomicInteger、Go sync/atomic 和 CPython Runtime 内部的 _Py_atomic_*,从 Runtime 一直看到 CPU。

讨论

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