JMM Java 内存模型


Java 虚拟机规范中定义了 Java 内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的并发效果,JMM 规范了 Java 虚拟机与计算机内存是如何协同工作的: 规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

如果你想设计表现良好的并发程序,理解 Java 内存模型是非常重要的。

Java 内存模型

Java 内存模型把 Java 虚拟机内部划分为线程栈和堆。这张图演示了 Java 内存模型的逻辑视图。其中 调用栈和本地变量存放在线程栈上,对象存放在堆上。

  • 一个本地变量可能是原始类型,在这种情况下,它总是 “呆在” 线程栈上。
  • 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
  • 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上。
  • 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
  • 静态成员变量跟随着类定义一起也存放在堆上。
  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。 如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

硬件内存架构

现代硬件内存模型与 Java 内存模型有一些不同。理解内存模型架构以及 Java 内存模型如何与它协同工作也是非常重要的。

现代计算机硬件架构的简单图示:

现代计算机通常是多核,每个 CPU 都包含一系列的寄存器(CPU 内部,读取速度特别快),另外每个 CPU 一般还会有一个高速缓存层,最后就是主存了,主存一般都会比较大,所有的 CPU 都可以访问主存。我们可以发现,寄存器和缓存层是 CPU 独享的,而主存则是所有 CPU 共享的。

通常情况下,当一个 CPU 需要读取主存时,它会将主存的部分读到 CPU 缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当 CPU 需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

但是这样就会有一些问题:

  1. 缓存一致性问题 :在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,那同步回到主内存时以谁的缓存数据为准呢?

这个主要依靠处理器访问缓存时遵循一些协议来保证,例如 MSI、MESI(IllinoisProtocol)、MOSI 等

  1. 指令重排序问题: 为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化(JVM 也会做类似的操作),这样的话如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。

Java 内存模型和硬件内存架构之间的桥接

显然 Java 内存模型与硬件内存架构之间存在差异。硬件内存架构并没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出现在 CPU 缓存中和 CPU 内部的寄存器中。如下图所示:

从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存(Main Memory)中
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是一个抽象概念,并不真实存在,本地内存中存储了该线程以读 / 写共享变量的拷贝副本。
  • Java 内存模型中的线程的工作内存(working memory)是 cpu 的寄存器和高速缓存的抽象描述

JMM 模型下的线程间通信

线程间通信必须要经过主内存。 换句话说如果线程 A 与线程 B 之间要通信的话,首先线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去,然后线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。

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

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

同时对以上操作制定了一些规则:

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

Java 内存模型解决的问题

Java 内存模型建立所围绕的问题:

  1. 在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)
  2. 多线程写同步问题与原子性(多线程竞争 race condition)。

多线程读同步与可见性

线程缓存导致的可见性问题:多线程中一个线程更新共享对象对其它线程来说可能是不可见的。解决此类可见性的三个关键字 volatile、synchronized 及 final:

  1. volatile 关键字:volatile 关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用 volatile 变量前都立即从主内存刷新。因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
  2. synchronized 关键字:前面有讲过 lock/unlock 操作需要遵守的规则 “如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值”
  3. final 关键字:被 final 修饰的字段在构造器中一旦被初始化完成,那么在其他线程就能看见 final 字段的值(无须同步)

重排序导致的可见性问题:编译器和处理器为了提高并行度会对代码和 CPU 指令进行重排序。重排序可能会导致多线程程序出现内存可见性问题(举例说明)。对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障指令来禁止特定类型的处理器重排序,从而为程序员提供一致的内存可见性保证。

多线程写同步与原子性

// 未完待续

happens-before

volatile 及 synchronized 内存语义

Copyright © jverson.com 2019 all right reserved,powered by Gitbook 17:11

results matching ""

    No results matching ""