文章目录
  1. 1. 缓存与一致性
  2. 2. JMM
    1. 2.1. 内存间交互操作
    2. 2.2. Happens-before
    3. 2.3. 指令重排序
      1. 2.3.1. 数据依耐性
      2. 2.3.2. as-if-serial
      3. 2.3.3. 内存屏障

Java内存模型,即 Java Memory Model(JMM),定义了 Java 虚拟机在计算机内存的工作方式。现在的 Java 内存模型主要源于1.5版本。

缓存与一致性

在计算机中,不同硬件的处理速度不同,往往有几个数量级的差距。比如 CPU 的处理速度往往高于内存数个数量级。因此计算机体系结构中引入了告诉缓存(Cache)放在内存和处理器之间作为缓冲。将 CPU 可能将要访问的数据先放在缓存中,因为访问缓存的速度远远高于直接访问内存,因此能加速运算。

高速缓存 虽然高速缓存解决了 CPU 和内存之间的速度问题,但是引入了另一个问题:缓存一致性。在多处理系统中,每一个处理器都有自己独立的缓存,但是整个系统共享一个内存,因此需要保证每个 CPU 读写的数据一致。常见的一致性协议有 MSI、MESI、MOSI以及 Dragon Protocol 等。

JMM 中定义的内存访问操作与计算机的高速缓存访问是类似的。

JMM

JMM 定义了程序中各个变量的访问规则。JMM 规定,所有的变量都存储在主内存中,但是每个线程还会有自己的工作内存,其中保存该线程使用到的变量副本,并且线程对内存的操作必须在工作线程中进行,不能直接访问主内存变量。如图所示:

JMM内存模型 在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。在共享内存的模型中,线程之间通过共享内存中的一个公共状态来进行通信。在消息传递模型中,通过显示的发送消息来通信。Java线程之间的通信采用的是过共享内存模型,即 JMM。

线程之间的通信必须要经历将要发送的消息通过共享变量的方式写入主内存,再被其他线程读取的过程。

内存间交互操作

为了保证其他线程读取的是最新写入的数据,因此 Java 内存模型定义了如下几种操作:

  1. lock: 作用于主内存的变量,把一个变量标识为一条线程独占状态;
  2. unlock: 作用于主内存的变量,解锁占用状态,允许被其他线程锁定;
  3. read: 作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  4. load: 作用于工作内存的变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中;
  5. use: 作用于工作内存的变量,把工作内存中的变量传递给虚拟机执行引擎,用于执行指令;
  6. assign: 作用于工作内存的变量,把执行引擎的值赋予工作内存变量;
  7. store: 作用于工作内存的变量,把工作内存的一个变量传递给主内存,以便随后的 write 的操作;
  8. write: 作用于主内存的变量,把 store 操作从工作内存中一个变量的值传送到主内存的变量中。

Happens-before

从 jdk5 开始,java 使用新的 JSR-133 内存模型,基于 happens-before 的概念来阐述操作之间的内存可见性。
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行 read 和 load 操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行 store 和 write 操作。Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是 read 和 load 之间,store 和 write 之间是可以插入其他指令的。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 不允许 read 和 load、store 和 write 操作之一单独出现
  • 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量。即就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
  • 一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。

指令重排序

在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。重排序分成三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

数据依耐性

如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。
编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

内存屏障

为了保证内存的可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java 内存模型把内存屏障分为 LoadLoad、LoadStore、StoreLoad和StoreStore 四种。

文章目录
  1. 1. 缓存与一致性
  2. 2. JMM
    1. 2.1. 内存间交互操作
    2. 2.2. Happens-before
    3. 2.3. 指令重排序
      1. 2.3.1. 数据依耐性
      2. 2.3.2. as-if-serial
      3. 2.3.3. 内存屏障