JVM-Java内存模型


前言

Java 内存模型(JMM)是一种虚拟机规范,定义程序中各种变量(不包括局部变量、方法参数等线程私有的变量)的访问规则,屏蔽了底层硬件与操作系统的内存访问差异,保证程序在各平台下对内存访问的一致效果。

主内存与工作内存

JMM 规定程序所有的变量都存储在主内存(main memory)中,而每个线程都有各自的工作内存(working memory),用于保存该线程所使用的变量的主内存副本。线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的数据,线程间也无法访问对方的工作内存。线程、主内存、工作内存的交互关系如图:

线程、主内存、工作内存

内存间交互操作

JMM 定义了 8 种内存交互操作,这些操作是原子的。对于 longdouble 64位的类型,允许虚拟机将没有被 volatile 修饰的变量划分为两次 32 位操作来进行,这就是所谓的 “longdouble 的非原子协定”。

操作

  • lock(锁定) 作用于主内存的变量,它把一个变量标志为一条线程独占的状态。

  • unlock(锁定) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。

  • read(读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。

  • load(载入) 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use(使用) 作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  • assign(赋值) 作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储) 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。

  • write(写入) 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

规则

  • 把变量从主内存拷贝到工作内存,需要顺序执行 readload 操作;把变量从工作内存同步回主内存,就要顺序执行 storewrite 操作。只要求顺序执行操作,不要求连续。也就说,在 readload 之间,storewrite 之间,可以插入其它指令。
  • 不允许 readloadstorewrite 操作之一单独出现。
  • 不允许线程丢弃它最近的 assign 操作,即变量在工作内存改变了之后必须把变化同步回主内存。
  • 不允许线程无故(没发生过 assign 操作)地把数据从工作内存同步回主内存。
  • 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(loadassign)的变量。
  • 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
  • 对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 loadassign 操作以初始化变量的值。
  • 一个变量事先没有被 lock 操作锁定,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 storewrite 操作)。

volatile

可见性

volatile 变量能够保证,一个线程修改了这个变量值,新值会立即同步回主内存,并且其它线程在读取该变量前,会从主内存刷新变量值。它解决了变量在各个线程的工作内存中的不一致问题,但由于 Java 里的运算操作并非原子操作,导致 volatile 在并发下一样是不安全的。

禁止指令重排优化

JMM 的 “线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),保证了代码在执行过程中,所有依赖赋值结果的地方都能够获取到正确的结果,在单线程下能够正确运行。但编译器和处理器为了优化计算,可能会对代码进行指令重排,与预期的代码执行顺序不同。

volatile 的另一个语义就是能够禁止指令重排。通过插入特定类型的内存屏障(Memory Barrier),指令重排时不能把后面的指令重排序到内存屏障之前的位置。

并发特征

原子性(Atomicity)

JMM 保证了原子性的变量操作包括 readloadassignusestorewrite。对于基本类型的访问,读写都具备原子性(例外 longdouble 的非原子性协定)。

JMM 还提供了 lockunlock,解决更大范围的原子性保证。通过字节码指令 monitorentermonitorexit 来隐式地使用这两个操作。对应到 Java 代码则是 synchronized 关键字,所以 synchronized 块之间的操作时原子的。

可见性(Visibility)

volatile:保证新值能立即同步回主内存,以及每次使用前都会从主内存刷新。

synchronized:由基本交互操作的规则,对一个变量执行 lock 操作,会清空工作内存中此变量的值;unlock 之前,会先把变量同步回主内存。

finalfinal 修饰的字段在构造器中一旦被初始化完成,并且构造器没有把 this 的引用传递出去,其他线程中就能看见 final 字段的值。无需同步就能被其它线程正确访问。

有序性(Ordering)

volatile:禁止指令重排。

synchronized:一个变量在同一时刻只允许一条线程对其进行 lock 操作。

先行发生原则(happens before)

​ 先行发生原则是判断数据是否存在竞争,线程是否安全的非常有用的手段。

  • 程序次序规则(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() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

  • 对象终结规则(Finalizer Rule)

    一个对象的初始化完成先行发生于它的 finalize() 方法的开始。

  • 传递性(Transitivity)

    如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。

参考

​ [1] 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版). 12.3 Java内存模型