2.3 语言内存模型:原子性、可见性与 Happens-Before
CPU、编译器和 JIT 可以重排操作并使用 cache/register。单线程只需保持 as-if-serial 的可观察结果,多线程若没有同步,另一个线程不一定按源码顺序看到写入。
三个问题要分别回答
Atomicity:操作是否会被观察为不可分割?
Visibility:一个线程何时必须看到另一个线程的写?
Ordering:哪些操作之间禁止重排或必须按关系观察?count++ 即使对 int 的单次读写各自原子,也由 read-modify-write 多步组成,不是原子增量。volatile 能提供特定可见性与顺序保证,不会把复合操作自动变成事务。
Happens-before 建立可见性契约
Java Memory Model 用 happens-before 限制读操作可以观察哪些写入。只有一个写入者且没有竞争写入时,写入 happens-before 后续读取,就能保证这个值可见。多个写入者下不能概括成“总能读到墙钟最新值”:读不能观察 happens-after 自己的写,也不能越过一个按 happens-before 位于候选写与读之间的写入;完整执行还需满足其他一致性规则。常见关系来自:
- 同一线程 program order;
- monitor unlock happens-before 后续对同一 monitor 的 lock;
- volatile write happens-before 同步顺序中后续对同一字段的 read;
- Thread.start 之前动作 happens-before 新线程动作;
- 线程全部动作 happens-before 另一个线程通过 join 检测到它终止后返回。
Happens-before 是偏序,不是墙钟先后。不同线程的冲突访问中,只要至少一个是写入,且两者未被 happens-before 排序,就构成 Java data race。墙钟上看起来一前一后,并不能建立这个关系。
Safe publication 是对象正确性的组成部分
volatile reference、共同的锁、适当的线程安全容器或类初始化,都能通过规定的顺序发布构造状态。正确构造的 final 字段还有特殊初始化保证:即使某些引用发布路径存在竞态,获得该引用的线程仍能看到这些字段的初始化值。这不是通用的同步边;在构造器中泄露 this 可能失去这项保护。
仍应使用明确的发布机制,保证引用本身的传递与非 final 状态可靠。不可变对象能减少后续同步;final 修饰列表引用,也不会让列表元素自动不可变。
动手检查 happens-before 路径
把下面代码保存为 PublicationDemo.java,用 JDK 17 或更高版本运行 java PublicationDemo.java:
class PublicationDemo {
static int value;
public static void main(String[] args) throws InterruptedException {
Thread writer = new Thread(() -> value = 42);
writer.start();
writer.join();
System.out.println(value);
}
}输出必须是 42:写入先于线程终止,join 检测到终止后返回,随后主线程才读取。对这条执行路径,value 不需要是 volatile。删掉 join 就失去这个保证,偶尔读到 42 不能验证无同步版本正确。加 sleep 只是多留一点时间,买不到同步契约。
Atomics 需要理解操作语义
CAS 循环:
read old
compute new
compareAndSet(old, new)
if failed, retry它适合简单状态转换,但高争用下会反复失败;ABA、对象生命周期和多字段不变量仍需额外设计。Atomic 类不等于任意组合操作线性化。
较低层语言提供 relaxed/acquire/release/sequentially-consistent order。越弱的 order 给实现更多优化空间,也更难证明。除非有基准和正式不变量,优先使用锁、channel 或成熟并发结构。
Data-race-free 的语言保证不同
Java 对存在 data race 的执行仍规定了允许行为,但结果可能反直觉,也未必崩溃。C/C++ 对普通非原子存储的数据竞争规定为 undefined behavior。Rust 安全类型系统防止普通安全代码产生 data race,但逻辑 race、死锁和 async cancellation 仍存在。
在启用 GIL 的 CPython 构建中,GIL 限制同一解释器内 Python 字节码的同时执行,不把多步业务操作变成原子事务;扩展代码可以释放它。CPython 也提供自由线程构建,必须先核对构建与运行配置,再判断是否有 GIL。共享状态仍应按需求使用锁、队列和明确的访问协议。
并发正确性验证
- 写出共享变量及保护它的唯一机制;
- 定义状态转换的线性化点;
- 对发布、关闭和取消路径建立 happens-before;
- 使用 stress test、race detector 和模型检查发现少见交错;
- 不用 sleep 猜线程顺序;
- 性能优化前保留一个易证明的锁实现作为基线。
下一章比较几种更高层并发模型,看看它们如何限制共享状态和交互方式。
参考资料
Oracle, JLS: Threads and Locks
C++ working draft, Data Races
Rust, Fearless Concurrency
Python, Free Threading