Java 内存模型
Table of Contents

本文是《深入理解java虚拟机》的读书笔记

主内存和工作内存

Java 内存模型规定所有变量都存储在主内存中(类比物理内存),每条线程还有自己的工作内存(类比处理器的高速缓存),线程的工作内存保存了该线程使用到的变量的主内存副本拷贝(仅保存用到的字段),线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程直接也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要主内存完成。

java 内存模型

内存间交互操作

主内存和工作内存之间有8种原子操作

  1. lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  2. unlock:解锁被lock的变量
  3. read:作用于主内存的变量,把一个变量的值从主内存传输到线程的工作线程中,以便随后的load使用
  4. load:作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
  5. use:作用于工作内存的变量,把工作内存中的变量值传递给执行引擎
  6. assign:作用于工作内存的变量,把从执行引擎得到的值赋给工作内存中的变量
  7. store:作用于工作内存的变量,把工作内存中的一个变量值传送到主内存中,以便随后的write操作使用
  8. write:作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中

对于volatile型变量的特殊规则

  1. volatile 变量在各个线程中都是一致的(这不能保证并发安全,因为读写操作不能保证是原子性的)
  2. 禁止指令重排序优化

volatile 变量的读操作性能消耗跟普通变量几乎没有差别,写操作会慢一点,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

volatile 的语义实现主要是通过设计原子操作的特殊规则实现的。

  1. 每次修改 volatile 变量马上写回主内存
  2. 每次读取 volatile 变量从内存中读取最新的值
  3. 通过内存屏障指令防止指令重排序优化

原子性,可见性和有序性

重排序

大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。 除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。

重排序分成三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种:

内存屏障:一个实现是通过 lock 指令强制将前面操作完成的修改从 CPU 的 cache 中写入到内存。

内存屏障

Happens-before 关系

happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。 下面介绍几个与理解 ConcurrentHashMap 有关的 happens-before 关系法则:

  1. 程序次序法则:如果在程序中,所有动作 A 出现在动作 B 之前,则线程中的每种动作 A 都 happens-before 于该线程中的每一个动作 B。
  2. 监视器锁法则:对一个监视器的解锁 happens-before 于每个后续对同一监视器的加锁。
  3. Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
  4. 传递性:如果 A happens-before 于 B,且 B happens-before C,则 A happens-before C。