Skip to content

Java 内存模型(JMM)总结

Published: at 16:54:20

主内存和工作内存

JAVA内存模型的主要目的是为了定义程序各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取变量这样的底层细节。

JMM规定了所有的变量都存储在了主内存,每条线程还有自己的工作内存,工作内存保存的是线程使用到的主内存变量的副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存的变量。线程之间也无法访问对方的工作内存,线程之间的变量值传递都需要通过主内存来完成。 untitled.jpg

内存间的交互操作

JAVA内存模型定义了8种操作来完成主内存和工作内存之间变量的交互,虚拟机实现时必须要保证每一种操作都是原子的

  1. Lock(锁定)

    🐖作用于主内存的变量

    把一个变量标识为一条线程独占的状态

  2. unlock(解锁)

    🐖作用于主内存的变量

    把处于锁定状态的变量释放锁,缩放锁后其他线程才可以锁定

  3. read(读取)

    🐖作用于主内存的变量

    把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用

  4. load(载入)

    🐽作用于工作内存的变量

    把 read 操作从主内存中读取到的变量值放入工作内存的变量副本中

  5. use(使用)

    🐽作用于工作内存的变量

    把工作内存中的变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作

  6. assign(赋值)

    🐽作用于工作内存的变量

    把一个从执行引擎收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时会执行这个操作

  7. store(存储)

    🐽作用于工作内存的变量

    把工作内存中的一个变量值传送到主内存中,以便随后的 write 动作使用

  8. write(写入)

    🐖作用于工作内存的变量

    把 store 操作从工作内存得到的变量的值写入到主内存的变量中

如果把一个变量从主内存复制到工作内存,那就要顺序的执行 read 和 load 操作,JAVA 内存模型只要求上述两个操作必须顺序执行,但并没有保证一定连续,也就是说两个操作之间是可能被插入其他指令的。

JAVA内存模型还规定了在执行以上8种基本操作时必须满足以下规则:

  1. 不允许 (read 、load) 和 (store、write) 操作之一单独出现
  2. 不允许一个线程丢弃它最近的 assign 操作,即变量在工作内存中改变后必须同步到主内存中
  3. 不允许线程无原因(没有 assign 操作)的把数据从工作内存同步到主内存中
  4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 和 assign 操作)的变量,也就是说,在对一个变量实施use和store之前,必须先执行load和assign
  5. 一个变量同一时刻只能被一个线程lock,但是可以被同一线程lock多次,unlock也必须被执行相同次数
  6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引起使用这个变量前,需要重新执行load或assign操作初始化变量的值
  7. 如果一个变量没有被lock,则不允许执行unlock,不允许一个线程unlock另一个线程锁定的变量
  8. 对一个变量执行unlock之前,必须先将此变量的值同步会主内存中(store、write操作)

volatile关键字

定义一个 volatile 变量之后,它会具备两种特性:保证可见性、禁止指令重排序

保证可见性

可见性是指,当一个变量被 volatile 修饰后,一个线程对变量修改的新值会立即同步到主内存中,以及每次使用前立即从主内存刷新。这样其他线程就可以立即获取到最新的值

但是 volatile 并不能保证变量在多线程环境下的安全性,尤其是在不符合以下两种运算场景中时:

  1. 运算结果并不依赖变量的当前值,或者能够确保只有一个线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束

禁止指令重排序

指令重排序的意思就是虚拟机不会保证执行的顺序是和代码顺序完全一致的。也就是JAVA内存模型中描述的“线程内表现为串行的语义(WithinThread As-If-Serial Semantics)”

例如代码:

static int x = 0, y = 0;
static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {
    Thread one = new Thread(new Runnable() {
        public void run() {
            a = 1;
            x = b;
        }
    });

    Thread other = new Thread(new Runnable() {
        public void run() {
            b = 1;
            y = a;
        }
    });
    one.start();other.start();
    one.join();other.join();
    System.out.println(“(” + x + “,” + y + “)”);
}

输出的结果可能是(1,0),(0,1),也有可能是(0,0),而(0,0)这种结果就很有可能是出现了指令重排序,a = 1 和 x = b 这两条指令没有被按照顺序执行。

原子性(Atomicity)

JAVA内存模型保证变量原子性的操作有read、load、use、assign、store、write。JAVA内存模型还提供了更大范围的原子性保证 :lock 和 unlock 操作,尽管虚拟机没有给用户开放使用,但是提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式的使用这个操作,这两个字节码指令反应到JAVA代码中就是同步块——synchronized 关键字。

可见性(Visibility)

可见性是指,当一个变量被 volatile 修饰后,一个线程对变量修改的新值会立即同步到主内存中,以及每次使用前立即从主内存刷新。这样其他线程就可以立即获取到最新的值

除了 volatile 关键字,synchronized 和 final 关键字也可以保证可见性。

同步块(synchronized)的可见性保证是因为对一个变量进行 unlock 之前,必须先把此变量同步回主内存中(JAVA内存模型规定的内存交互规则第8条)

final的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那么其他线程中就能看见 final 字段的值。

对于 final 域,编译器和处理器要遵守两个重排序规则:

  1. final 写:“构造函数内对一个final域的写入”,与“随后把这个被构造对象的引用赋值给一个引用变量”,这两个操作之间不能重排序。
  2. final 读:“初次读一个包含final域的对象的引用”,与“随后初次读对象的final域”,这两个操作之间不能重排序。

代码示例:

public final class FinalExample {
    final int i;
    public FinalExample() {
        i = 3;     // 1
    }

    public static void main(String[] args) {
        FinalExample fe = new FinalExample();    // 2
        int ele = fe.i;                          // 3
    }
}

说明: 操作1与操作2符合重排序规则1,不能重排;操作2与操作3符合重排序规则2,不能重排。

有序性(Odering)

JAVA 提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性。

volatile 关键字本身就包含了禁止重排序的语义

synchronized 关键字则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获取的。

先行发生原则(happens-before)

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

happens-before原则定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

happens-before 定义的规则:

  1. 程序次序规则: 在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 管程锁定规则: 一个unLock操作先行发生于后面对同一个锁的lock操作;
  3. volatile变量规则: 对一个volatile变量的写操作先行发生于后面对这个变量的读操作;
  4. 线程启动规则: Thread对象的start()方法先行发生于此线程的每个一个动作;
  5. 线程终止规则: 线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  6. 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt()方法检测是否发生中断;
  7. 对象终结规则: 一个对象的初始化完成先行发生于他的finalize()方法的开始;
  8. 传递规则: 如果操作A先行发生于操作B,而操作B又先行发生于操作C,可以得出操作A先行发生于操作C的结论;