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 中,如果 write W happens-before read R,R 必须看到 W 或之后的写。常见边:
- 同一线程 program order;
- monitor unlock happens-before 后续对同一 monitor 的 lock;
- volatile write happens-before 后续对同一字段的 read;
- Thread.start 之前动作 happens-before 新线程动作;
- 线程全部动作 happens-before 成功 join 返回。
Happens-before 是偏序,不是墙钟先后。两个访问在时间上看似一前一后,若没有建立关系,程序仍有 data race。
Safe publication 是对象正确性的组成部分
构造完成的对象需要通过同步机制发布:final field 正确构造语义、volatile reference、lock、线程安全 collection 或类初始化。把 this 在构造器中泄露给其他线程,会破坏 final field 等保证。
不可变对象减少后续同步,却仍需要安全发布。final 只禁止字段重新赋值,不会让字段引用的可变 list 自动不可变。
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++ 中普通内存 data race 通常导致 undefined behavior。Rust 安全类型系统防止普通安全代码产生 data race,但逻辑 race、死锁和 async cancellation 仍存在。
CPython 的 GIL 只限制同一解释器中 Python bytecode 的并行执行细节,不把多步业务操作变成原子事务;扩展模块会释放 GIL,其他实现也不同。不能用 GIL 替代 lock/queue 和共享状态设计。
并发正确性验证
- 写出共享变量及保护它的唯一机制;
- 定义状态转换的线性化点;
- 对发布、关闭和取消路径建立 happens-before;
- 使用 stress test、race detector 和模型检查发现少见交错;
- 不用 sleep 猜线程顺序;
- 性能优化前保留一个易证明的锁实现作为基线。
下一章比较几种更高层并发模型,看看它们如何限制共享状态和交互方式。
参考资料
- Oracle, JLS: Threads and Locks
- C++, Memory model
- Rust, Fearless Concurrency