JMM内存模型的可见性与有序性原理

JMM内存模型的可见性与有序性原理

JVM与JMM

首先,不要把JVM内存模型与JMM内存模型搞混淆了

我们常说的JVM内存模式指的是JVM的内存分区,而Java内存模式是一种虚拟机规范

Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果

JMM规范了Java虚拟机与计算机内存是如何协同工作的:

一个线程如何与何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量

Java内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别,在任何计算机上,JMM都可以抽象成线程与内存

CPU多级缓存结构

CPU的缓存结构我称之为JMM的范本,是JMM的基石

JMM内存模型的可见性与有序性原理

  • 多CPU:现代计算机一般为多核或多CPU,这也是多线程的基石
  • CPU寄存器(CPU Registers):每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存
  • 高速缓存cache:由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。每个CPU可能有一个CPU缓存层,一些CPU还有多层缓存。在某一时刻,一个或者多个缓存行(cache lines)可能被读到缓存,一个或者多个缓存行可能再被刷新回主存
  • 内存:一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。
  • 运作原理:通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存

如图,JMM定义了主存、本地内存的抽象概念,底层实际上对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等

JMM内存模型的可见性与有序性原理

  • 线程之间的共享变量存储在主内存(Main Memory)中

  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了CPU缓存、写缓冲区、CPU寄存器以及其他的硬件和编译器优化。本地内存中存储了该共享变量的拷贝副本

  • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存

JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化(指令重排、组合)的影响

缓存一致性问题

在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)

当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?

JMM内存模型的可见性与有序性原理

为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等

指令重排序问题

为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化

处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。

因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证

与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化

多线程下指令重排会影响运行结果的正确性,适当使用valatile禁用指令重排是必要的

JVM和硬件内存架构

Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中

部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中

如下图所示:

JMM内存模型的可见性与有序性原理

JMM下线程间的通信

关于主内存与工作内存(本地内存)之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种原子操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中

对应在JMM中,示例图

JMM内存模型的可见性与有序性原理

JMM多线程下解决缓存不一致问题

  • 总线加锁(低性能)

    CPU从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他CPU没法去读或写这个数据,直到这个CPU使用完数据释放锁之后其他CPU才能读取该数据

  • MESI缓存一致协议

    多个CPU(核)从主内存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步会主内存,其他CPU通过总线嗅探机制可以感知主线程中数据的变化从而将自己缓存(工作内存)中的数据失效掉

    JMM内存模型的可见性与有序性原理