内存模型

Catalogue   

概述

Java 内存模型 (Java Memory Model) 定义了 JVM 如何正确访问计算机主内存。JMM 指定了不同线程如何以及何时可以看到其他线程写入到共享变量的值,以及如何在必要时同步访问共享变量。

Java 多线程之间通信一般有两种方式: 共享内存消息传递。Java 的并发采用共享内存的方式,共享内存通信方式对于程序员而言总是透明隐式进行的。

JMM 关键技术点都是围绕着多线程的原子性可见性有序来讨论的。JMM 解决了可见性和有序性的问题,而锁解决了原子性的问题。

Java 内存模型的可见性问题的底层实现是通过内存屏障 (memory barriers) 实现。

现代计算机内存模型:

Java内存模型:

具体操作

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

规则

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

内存结构

JVM运行时内存结构

程序计数器:

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

Java栈(虚拟机栈):

栈描述的是Java方法执行的内存模型。每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。
每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

  1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
  2. 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
  3. 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

堆区:

堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。

  1. 存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
  2. jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

方法区:

  1. 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
  2. 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

在JAVA虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个
方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。

线程栈

JVM 内存结构中有一个非常重要的内存区域叫做线程栈 , 每个线程的栈大小可以通过设置 JVM 参 数-Xss, -Xss128k 表示每个线程堆栈大小为 128K,
JDK1.5 默认值为 1M。(Android也是,每当新建一个线程,native就会划分1M左右空间出来)

线程栈内存存储了基本类型变量和对象引用,当访问了对象的某一实例变量时,通过在栈中获得对象引用再获取变量的值,然后将变量的值拷贝至线程的工作内存。

每个线程 (处理器) 都有工作内存,工作内存存了该线程以读写共享变量的副本。工作内存是 JMM 抽象概念 , 并不真实存在。

  • read and load 从主存复制变量到当前工作内存;
  • use and assign 执行代码,改变共享变量值;
  • store and write 用工作内存数据刷新主存相关内容;
  • 其中 use and assign 可以多次出现。

但是这一些操作并不是原子性,也就是在 read load 之后,如果主内存 count 变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,
所以计算出来的结果会和预期不一样。

可见性

可见性指的是一个线程对变量的写操作对其他线程后续的读操作可见。

由于现代 CPU 都有多级缓存,CPU 的操作都是基于高速缓存的,而线程通信是基于内存的,这中间有一个Gap, 可见性的关键还是在对变量的写操作之后能够在某
个时间点显示地写回到主内存, 这样其他线程就能从主内存中看到最新的写的值。

volatile,synchronized(隐式锁), Lock(显式锁),Atomic(原子变量)这些同步手段都可以保证可见性。可见性底层的实现是通过加内存屏障实现的:

  • 写变量后加写屏障,保证 CPU 写缓冲区的值强制刷新回主内存;
  • 读变量之前加读屏障,使缓存失效,从而强制从主内存读取变量最新值。

指令重排序

对于处理器而言,一条汇编指令的执行时分为很多步骤的。在多处理器下,一个汇编指令不一定是原子操作的。
为提高CPU利用率,加快执行速度,将指令分为若干个阶段,可并行执行不同指令的不同阶段,从而多个指令可以同时执行。

数据依赖性

上面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。前面提到过,编译 器和处理器可能会对操作做重排序。编译器和处理器在重排序时,
会遵守数据依赖性,编译器和 处理器不会改变存在数据依赖关系的两个操作的执行顺序。这里所说的数据依赖性仅针对单个处 理器中执行的指令序列和单个线程中执行的操作,
不同处理器之间和不同线程之间的数据依赖性 不被编译器和处理器考虑。

as-if-serial

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

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。

happens-before 原则

happen 与 before 规则阐述操作之间的内存可见性,目的都是为了在不改变程序的语义情况下提 高程序的并行度。在 JMM 中,如果一个操作执行的结果需要对另一个操作线程,
那么这两个操作之间必须存在 happen-before 关系。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  • 锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作;
  • volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  • 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C;
  • 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每个一个动作;
  • 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行;
  • 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始;
  • 程序顺序规则:一个线程中的每个操作,happen-before 于该线程中的任意后续操作;
  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁;
  • Volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读;
  • 传递性:如果 A happens-beforeB , 且 B happens-before C, 那么 A happens-before C;
  • Start 规则: 如果线程 A 执行操作 ThreadB.start()(启动线程 B), 那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。

参考

  • 《Time,Clocks and the Ordering of Events in a Distributed System》