0%

java 内存模型

概述

java 内存模型(Java Memory Model, JMM)是 java 虚拟机定义的一种抽象的模型,主要是用来屏蔽各种硬件和操作系统的内存访问差异,以实现让 java 程序在各种平台下都能达到一致的内存访问效果。其模型大致如下图所示:

java内存模型

主内存和工作内存

在 java 内存模型中,主内存是各个线程共享的,主要用于存在程序中创建的对象。与 java 内存区域(JVM 内存模型)中的堆可以进行类比,两者的功能类似。

工作内存,则是各个线程独享的,各个线程之间互不干扰。与 java 内存区域中的虚拟机栈类似,都是存放着本线程使用到的局部变量,如 int、long 等基本类型。需要注意的是,使用 Interger、Long 等包装类型或其他类对象,其对象实例是在主内存中,而持有该对象的引用则是在工作内存中。

当需要使用主内存的对象时,会将主内存的对象拷贝到线程的工作内存中,即线程的工作内存持有主内存变量的副本,当线程对该变量执行完毕后,需要将新的变量值写入主内存,此时,也是先将结果放在工作内存中,然后再从工作内存中写入主内存。从这个角度来说,工作内存就是线程执行引擎与主内存之间的桥梁,无论是读取还是更新主内存的变量,都需要先通过线程的工作内存进行。

内存间的交互操作

工作内存和主内存需要频繁进行通信,java 内存模型也为此定义了 8 中原子操作来完成这种交互。

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

除了定义这 8 种操作之外,java 内存模型还定义这 8 种基本操作必须满足的原则:

  • 不允许 read 和 load 操作单独出现,也不允许 store 和 write 操作单独出现,也就是说 read 和 load 操作(store 和 write 操作)这两个操作是绑定的,必须成对出现。虽然如此,但是并没有说这两个操作必须连续出现,也就是说执行这两个操作中间可以执行其他的操作。如读取两个变量 a 和 b,执行的顺序可以是 read a -> read b -> load a -> load b
  • 不允许一个线程丢弃他的最近的 assign 操作,即变量在工作在工作内存中改变了之后必须把该变化同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步到主内存中。
  • 一个新的变量(不包括基本类型的局部变量)只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,即对一个变量实施 use、store 操作之前,必须先执行了 assign 和 load 操作。
  • 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一个线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果对一个变量执行 lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
  • 如果一个变量事先没有被 lock 操作锁定,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)。

java 内存模型特性

java 内存模型有三大特性:原子性、可见性、有序性。

原子性

原子性(Atomicity)是指一个操作是不可中断的,即一个操作一旦开始执行就必须等待其执行完成,中间不会插入其他的操作。即使在多线程的环境下也是如此。java 内存模型提供的 8 中基本操作都是原子性的(虽然对 long 和 double 这种 64 位的数据结构而言,部分操作可以不是原子性的,但是几乎所有的商业虚拟机都是将其实现成原子性的),这些基本的操作能控制一些比较简单的指令。而对于需要将一块代码块(多个程序语句)实现成原子性时,Java 代码中也提供了 synchronized 关键字来保证,其对应到字节码指令是 monitorentermonitorexit

可见性

可见性(Visibility)是指当一个线程修改了共享变量的值,其他线程能够立即得到这个修改。这主要是通过将更新后的变量值及时写入主内存,同时其他线程获取变量时直接从主内存获取到工作内存来实现的。在 java 中有 volatilesynchronizedfinal 三个关键字可以实现可见性。

  • volatile:被 volatile 修饰的变量,读取变量时,直接从主内存读取,更新变量时,立即同步到主内存。通过这种方式实现其可见性。
  • synchronized:其可见性是由 “对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)” 这个规则保证的。
  • final:被 final 修饰的字段(所在对象存在于主内存中)在构造器中一旦完成初始化并且构造器没有将 “this” 的引用传递出去,则其他线程能看到 final 字段的值。

有序性

有序性(Ordering)指在本线程内观察,所有的操作都是有序的(即串性执行)。而对于在一个线程中观察另一个线程,则操作是无序的,这主要是指 “指令重排“ 和 “工作内存与主内存同步延迟” 。

java 提供了 volatilesynchronized 两个关键字来保证线程之间操作的有序性。

  • volatile:通过禁止指令重排序来实现线程的有序性
  • synchronized:其有序性由 “一个变量在同一个时刻只允许一条线程对其进行 lock 操作” 这个规则来保证的。

先行发生原则

除了 volatilesynchronized 这两个关键字保证有序性之外,java 内存模型也默认提供并执行一些先行发生原则来保证有序性。

先行发生是 java 内存模型中定义的两种操作之间的偏序关系,如果操作 A 先行与操作 B 发生,则操作 A 执行的结果能够被操作 B 观察到。

先行发生原则包括以下几点:

  • 程序次序规则(Program Order Rule):在一个线程内,安装程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管城锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面(时间上的先后)对于同一个锁的 lock 操作。
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面(时间上的先后)对这个变量的读操作。
  • 线程启动规则(Thread Start Rule):Thread 对象的 start 方法先行发生于此线程的每个动作。
  • 线程终止规则(Thread Termination Rule):线程中所有操作都先行发生于对此线程的终止。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。可以通过 Thread.interrupted() 方法检测到是否有中断发生。也就是说, interrupt() 方法先于 Thread.interrupted() 方法执行。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,则操作 A 先行发生于操作 C。

参考资料

[1] 周志明,深入理解Java虚拟机:JVM高级特性与最佳实践[M],北京:机械工业出版社,2013