Java内存模型(JMM)与happens-before规则

Java内存模型概述

Java内存模型(Java Memory Model, JMM)是Java虚拟机规范中定义的一套规则,用于规范多线程环境下对共享变量的访问行为。JMM的主要目的是解决以下问题:

  1. 在多线程环境下,如何保证线程之间对共享变量的读写操作是可见的
  2. 如何防止指令重排序导致程序执行结果与预期不符
  3. 如何正确同步线程之间的操作顺序

JMM定义了线程与主内存之间的抽象关系:每个线程都有自己的工作内存(Working Memory),保存了该线程使用到的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量。

内存模型中的八大原子操作

JMM定义了8种原子操作来完成主内存与工作内存之间的交互:

  1. lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占状态
  2. unlock(解锁):作用于主内存变量,释放锁定状态
  3. read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存
  4. load(载入):作用于工作内存变量,把read操作得到的值放入工作内存的变量副本
  5. use(使用):作用于工作内存变量,把工作内存变量值传递给执行引擎
  6. assign(赋值):作用于工作内存变量,把从执行引擎接收的值赋给工作内存变量
  7. store(存储):作用于工作内存变量,把工作内存变量值传送到主内存
  8. write(写入):作用于主内存变量,把store操作得到的值放入主内存变量

happens-before规则

happens-before是JMM中最核心的概念之一,用于确定两个操作的执行顺序以及内存可见性。如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

happens-before的八大规则

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
  4. 线程启动规则:Thread对象的start()方法happens-before于此线程的每一个动作
  5. 线程终止规则:线程中的所有操作都happens-before于其他线程检测到该线程已经终止
  6. 线程中断规则:对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生
  7. 对象终结规则:一个对象的初始化完成happens-before于它的finalize()方法的开始
  8. 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C

实际应用示例

public class HappensBeforeExample {
    private int x = 0;
    private volatile boolean v = false;
    
    public void writer() {
        x = 42;    // 1
        v = true;  // 2
    }
    
    public void reader() {
        if (v) {   // 3
            System.out.println(x); // 4
        }
    }
}

在这个例子中:

  • 根据程序顺序规则,操作1 happens-before 操作2
  • 根据volatile变量规则,操作2 happens-before 操作3
  • 根据程序顺序规则,操作3 happens-before 操作4
  • 根据传递性规则,操作1 happens-before 操作4

因此,如果线程A执行writer()方法,线程B执行reader()方法,当线程B看到v为true时,保证能看到x的值为42。

内存屏障与指令重排序

为了实现happens-before规则,JVM会在适当的位置插入内存屏障(Memory Barrier)指令来禁止特定类型的处理器重排序。内存屏障分为四种:

  1. LoadLoad屏障:确保Load1的数据装载先于Load2及所有后续装载指令
  2. StoreStore屏障:确保Store1的数据对其他处理器可见先于Store2及所有后续存储指令
  3. LoadStore屏障:确保Load1的数据装载先于Store2及所有后续存储指令
  4. StoreLoad屏障:确保Store1的数据对其他处理器可见先于Load2及所有后续装载指令

常见同步原语的实现原理

volatile的实现

volatile变量的读写操作会插入以下内存屏障:

  • 写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障
  • 读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障

synchronized的实现

synchronized块使用monitorenter和monitorexit指令实现,在进入和退出时都会插入相应的内存屏障。

final字段的特殊处理

final字段的写入会在构造函数结束时插入StoreStore屏障,确保final字段的初始化对其他线程可见。

JMM的实践意义

理解JMM和happens-before规则对于编写正确的并发程序至关重要:

  1. 正确使用volatile、synchronized等同步原语
  2. 避免依赖数据竞争(Data Race)来实现线程通信
  3. 理解并发的可见性、原子性和有序性问题
  4. 设计线程安全的类时,合理规划内存可见性边界

通过遵循happens-before规则,开发者可以编写出既高效又正确性有保障的多线程程序。