本文为《深入理解 Java 虚拟机》的笔记。

Java 虚拟机规范中定义了一种 Java 内存模型,用来规避各种硬件和操作系统的内存访问差异,让 Java 在各种平台下都能达到一致的并发效果。Java 内存模型的主要目标是定义程序中各个变量的访问规则,在 JVM 中将变量存储到内存和从内存中取出变量这样的底层细节。

Java 内存模型的目标是定义程序中各个变量的访问规则,这里的变量 Variable 包含了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为后者是线程私有的,不会被共享。

Java 内存模型规定所有变量存储在主存 Main Memory 中,每个线程都有自己的工作内存 Working Memory。线程的工作内存中保存了被该线程使用到的主存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写内存中变量。不同线程无法访问对方工作内存的变量,线程间变量传递需要通过主存来完成。

内存间交互操作

主存和工作内存之间定义了如下的操作。

  • lock : 作用于主存变量,把变量标识为一条线程独占状态
  • unlock : 作用于主存变量,把处于锁定状态的变量释放
  • read : 作用于主存,把变量值从主存传输到线程的工作内存,以便 load 使用
  • load : 作用于工作内存的变量,把 read 操作从主存中得到的变量值放到工作内存的变量副本中
  • use : 作用于工作内存变量,把工作内存的变量传递给执行引擎,当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
  • assign : 作用于工作内存的变量,把从执行引擎收到的值赋值给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • store : 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后 write 操作使用
  • write : 作用于主内存的变量,把 store 操作从工作内存中得到的变量的值放到主内存变量

定义了这些操作之后,就有一些规则,必须满足。

  • 不允许 read, load, store, write 操作单独出现,必须组合出现
  • 不允许线程丢弃它最近的 assign 操作
  • 不允许线程无原因(无 assign 操作)将数据从线程的工作内存同步到主存中
  • 新变量只能在主存中诞生,不允许在工作内存中直接使用未被初始化 (load or assign) 的变量,对一个变量实施 use 和 store 操作之前,必须先执行过 assign 和 load 操作
  • 一个变量同一时刻只允许一条线程对其 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作变量才会解锁
  • 对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 和 assign 操作初始化变量的值
  • 如果变量没有 lock 操作,不允许 unlock 操作,也不允许 unlock 被其他线程锁定的变量
  • 对一个变量执行 unlock 操作前,必须把变量同步回主存中(执行 store 和 write 操作)

volatile 变量的规则

volatile 是 Java 虚拟机提供的最轻量的同步机制。当变量定义成 volatile 后,具备两种特性:

  • 此变量对所有线程可见,当一条线程对变量做出修改,新值对于其他线程来说是立即得知的
  • volatile 变量第二个语义是禁止指令重排序

指令重排序

为了提升执行速度,计算机在执行代码的时候会对指令进行重排序。

包括:

  • 编译器优化重排,包括 JVM,JIT 编译器,不改变单线程语义的情况下重新安排语句执行顺序
  • 指令并行重排,现代处理器采用指令级别并行技术 (Instruction-Level Parallelism,ILP)来将多条指令重叠执行,如果不存在数据依赖问题,处理器可以改变语句顺序。

指令重排序,指的是 JVM 优化指令执行的顺序,提高程序的运行速度,在不影响单线程程序执行结果的前提下,尽可能提高并发。

在 JDK1.5 之后,使用 volatile 变量禁止指令重排序。针对 volatile 修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏

double pi = 3.14;
double r = 2.1;
double area = pi*r*r;

代码在执行时,1->2->3,或者 2->1->3 对结果并没有影响,编译和运行时可能对 1,2 语句进行重排序。

JVM 内存屏障插入策略:

  • 每个 volatile 写操作的前面插入一个 StoreStore 屏障;
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障;
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障;
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。