跳到内容

2.3 语言内存模型:原子性、可见性与 Happens-Before

两条线程在纸面上按顺序读写,实际运行却偶尔看见旧值,工坊师傅让你们从语言允许的执行结果重新推理。

CPU、编译器和 JIT 可以重排操作并使用 cache/register。单线程只需保持 as-if-serial 的可观察结果,多线程若没有同步,另一个线程不一定按源码顺序看到写入。

三个问题要分别回答

text
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 循环:

text
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 猜线程顺序;
  • 性能优化前保留一个易证明的锁实现作为基线。

下一章比较几种更高层并发模型,看看它们如何限制共享状态和交互方式。

参考资料

Built with VitePress | Software Systems Atlas