跳转到内容

JMM 与 happens-before

用途:判断“这段代码在多线程下会不会读到旧值”时查阅

Java 内存模型(JMM)规定多线程程序中对共享变量的读写在何时相互可见,以及允许怎样的重排序。它不是 JVM 的内存布局(堆/栈那是运行时数据区),而是一套可见性与有序性的约束。

核心概念:

概念 含义
工作内存 线程对共享变量的本地缓存抽象(寄存器、CPU cache 的模型化)
主内存 共享变量的权威存储抽象
重排序 编译器与处理器在不改变单线程语义前提下调整指令顺序
happens-before 跨线程可见性的判定规则:A happens-before B,则 A 的结果对 B 可见
  1. 程序顺序:同一线程内,前面的操作 happens-before 后面。
  2. 监视器锁:unlock happens-before 后续对同一锁的 lock。
  3. volatile:对 volatile 变量的写 happens-before 后续读。
  4. 传递性:A hb B 且 B hb C,则 A hb C。
  5. 线程启动:Thread.start() happens-before 该线程内任何操作。
  6. 线程终止:线程内所有操作 happens-before 其他线程成功 join() 返回。
  7. 中断与对象终结:interrupt() 先于被中断线程检测到中断;构造函数结束先于 finalize()。

双重检查锁为什么需要 volatile

「双重检查锁为什么需要 volatile」小节链接
instance = new Singleton(); // 实际分三步:分配内存 → 初始化 → 引用赋值

若第 2、3 步重排,另一线程在第一次判空时可能拿到“未初始化完成”的对象。volatile 禁止这种跨越写的重排,并通过 happens-before 保证可见性。

误解 事实
加 volatile 就线程安全 volatile 只保证可见性与有序性,不保证原子性(i++ 仍会丢更新)
long/double 读写天然原子 未声明 volatile 的 64 位类型允许被拆成两次 32 位读写
synchronized 只是互斥 它同时提供可见性(解锁前的写对后续加锁者可见)
读到旧值是缓存问题 根因是重排序与可见性规则,不是“缓存没刷”
happens-before 等于时间先后 它是可见性约束,与物理时间顺序不同
  1. 不要手写基于 volatile 的无锁协议,优先使用 java.util.concurrent 现成组件。
  2. 安全发布对象的四类方式:静态初始化、volatile 字段、final 字段(正确构造)、加锁字段。
  3. 不变对象(final 字段且状态不可变)天然跨线程安全,优先用不可变数据。
  4. 出现“偶发读到旧值”的缺陷时,先检查是否缺少同步,而不是加 sleep 或重试。
误区 修正
把 JMM 当成堆栈等内存布局 它是可见性与有序性约束
认为 volatile 保证原子性 复合操作仍需锁或原子类
用 synchronized 只当互斥 它同时提供可见性
把“读到旧值”归因于 CPU 缓存 根因是重排序与 happens-before 缺失
  • 《Java 语言规范》第 17 章(Threads and Locks,JMM 正式定义)
  • Brian Goetz《Java 并发编程实战》第 16 章(Java 内存模型)
  • Doug Lea 关于 JMM 与实践的公开文章