16_02_Java内存模型
Java内存模型
一、什么是内存模型?
缓存一致性问题:
- 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory)
- 当多处理器对同一块主内存区域进行操作时,可能会导致各自的缓存数据不一致,这就是缓存一致性问题
内存模型:
- 内存模型,是指在特定的操作协议下,对特定的内存或缓存进行读写访问的抽象过程
- 内存模型,定义了如何以一种安全可靠的方式去读写访问内存或缓存
- 简答来讲,内存模型规定了什么时候读入数据、什么时候写入数据、读写的顺序的等内存访问的问题
缓存一致性协议:
- 为了解决缓存一致性问题,内存模型就需要引入一些缓存一致性协议,比如 MSI、MESI、MOSI 等
- 缓存一致性协议,并不是针对某种内存模型,而是一种通用的缓存同步规范
- Java 内存模型,是 JVM 定义的一个内存模型,使用的缓存一致性协议是 MESI 协议
二、为什么要有内存模型?
定义 Java 内存模型的目的:
- 屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果
定义Java内存模型的好处:
- Java 内存模型统一了底层不同架构平台上内存模型的接口
- 程序无需关注运行平台的差异,只需要关注 Java 内存模型提供了怎么样的内存保证
- 程序不用去考虑底层访问的性能问题,由 JVM 负责去优化,JVM 会根据硬件的特性去优化执行速度
比如说,Java 内存模型保证了 volatile 变量的写入是对所有线程可见的。
程序员只需知道 volatile 变量是所有线程可见的即可,至于内存模型底层是怎么实现的,用的时候无需了解。
三、Java 内存模型的设计
3.1 内存层次划分
内存层次:
- 主内存(Main Memory):主内存只有一个,所有变量数据都存储在主内存中
- 工作内存(Working Memory):每条线程都有自己的工作内存,使用的变量都是主内存变量的一份拷贝
内存操作:
- 线程对变量的所有操作(读取、赋值等)都是在自己的工作内存中进行
- 不同线程之间不能直接访问对方工作内存中的变量
- 线程之间的变量值共享,只能通过主内存来完成
3.2 内存间的交互操作
内存交互问题:
- 变量如何从主内存拷贝到工作内存?
- 变量如何从工作内存同步回主内存?
原子操作:
- lock(锁定):作用于主内存变量,锁定主内存中的变量,防止其他线程访问
- read(读取):作用于主内存变量,读取主内存中的变量
- load(加载):作用于工作内存变量,将 read 拿到的变量,拷贝到工作内存中
- use(使用):作用于工作内存变量,使用工作内存中的变量
- assign(赋值):作用于工作内存变量,对工作内存的变量进行赋值
- store(存储):作用于工作内存变量,将工作内存的变量传送到主内存
- write(写入):作用于主内存变量,将 store 传送出来的变量写入主内存变量
- unlock(解锁):作用于主内存变量,将主内存中变量的锁定状态解除
执行规则:
- 一个新的变量,只能在主内存中“诞生”
- 不允许 read 和 load、store 和 write 操作之一单独出现
- 一个变量在同一时刻只允许一条线程对其进行 lock 操作
- 对一个变量执行 lock 操作,那将会清空工作内存中此变量的值
- 对变量执行 unlock 操作前,必须先把此变量同步回主内存中
3.3 内存模型的并发设计
针对并发中出现的原子性、可见性、有序性问题,内存模型给了多种并发保证方式,来确保它们的安全性。
3.3.1 内存指令的并发保证
(1) 原子性:
- 由 Java 内存模型直接保证的原子性操作包括 read、load、use、assign、store、write
- 更大范围的原子性操作,Java 内存模型还提供了 lock 和 unlock 来满足
(2) 可见性:
- Java 内存模型依赖于主内存作为传递媒介,来实现可见性:
- 写入变量后,将值同步刷新回主内存,比如通过 unlock 操作实现
- 读取变量前,从主内存中获取最新值,比如通过 lock 操作实现
- 一个线程的操作结果对于另一个线程可见,那必然是经过了主内存的传递
(3) 有序性:
- Java 内存模型是通过按需禁止重排序来避免有序性问题的
- 通过底层指令集的某些指令来禁止重排序,Java 语言层面可以使用 volatile 关键字
3.3.2 关键字的并发保证
Java 语言中的并发关键字包括 synchronized、volatile、final。
(1) synchronized:
- 可以保证原子性、可见性、有序性
- 原子性:由“一个变量在同一时刻只允许一条线程对其进行 lock 操作”规则保证
- 可见性:由“对变量执行 unlock 操作前,必须先把此变量同步回主内存中”规则保证
- 有序性:由“一个变量在同一时刻只允许一条线程对其进行 lock 操作”规则保证,这条规则决定了持有同一个锁的两个同步块只能串行地进入
(2) volatile:
- 可以保证可见性和有序性
- 可见性:volatile 变量保证了新值可以立即同步回主内存中,以及每次使用前立即从主内存中刷新
- 有序性:volatile 关键字本身设计就包含了禁止指令重排序地语义
(3) final:
- 可以保证可见性
- 可见性:被 final 修饰的字段,在构造器中一旦正确安全初始化完成后(安全发布),就保证了它的可见性
并发关键字可以让程序自己选择是否要控制可见性和有序性。
3.3.3 先行发生原则(happens-before)的并发保证
Java 内存模型为了保证可见性和有序性,规定了一种先行发生原则(happens-before)。
先行发生原则定义:
- 想要保证执行操作 B 的线程,能够看到执行操作 A 的结果,A 和 B 必须满足 Happen-Before 原则
- 如果操作 A 发生于操作 B 之前,那么操作 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() 方法的调用先行发生于对此线程的中断检测
- 对象终结规则(Object Termination Rule):对象的初始化完成(构造函数执行结束)先行发生于它的 finallize() 方法的开始
- 传递性规则(Transitivity Rule):如果 A 先行于 B,B 先行于 C,那么 A 先行于 C
举几个例子:
(1) 管程锁定规则
管程锁定规则,规定了 unlock 操作先行发生于 lock 操作。
也就是说,上一个 unlock 操作之前产生的“影响”,对于下一个 lock 操作是可见的。
1 | // 线程 A |
假设线程 A 先拿到锁,对 count 变量进行了修改,得到 count = 1,然后执行 unlock 操作退出加锁区域。
接着线程 B 再执行 lock 操作去获取锁,线程 A 的 unlock 操作先于线程 B 的 lock 操作执行。
依据管程锁定规则,那么线程 B 就能看到线程 A 的修改,也就是线程 B 内可以看到 count = 1。
(2) 线程启动规则
线程启动规则,规定了 Thread 对象的 start() 方法先行发生于此线程的每一个动作。
也就是说,在 Thread 对象的 start() 方法执行前所产生的“影响”,线程内都是可见的。
1 | static int count = 0; |
主线程在启动子线程 B 的 start() 方法前的操作,即 count = 1,对于线程 B 内而言,是可见的。
也就是,主线程对 count 的修改,先行于线程 B 的 start() 方法,那么线程 B 内部就可以看到修改的结果。
3.3.4 初始化安全性的并发保证
内存模型还提供了一些在对象初始化方面的并发保证:
- 静态初始化器,可以保证对所有线程具有可见性
- 对于被正确构造的对象,所有线程都能看到构造函数给 final 域设置的值,包括 final 域引用对象内可达的值
1 | public class SafeStates { |
也就是说,由 SafeStates 创建的对象,它里面的 object 和 states,对于所有线程而言,都是可见的。
而且,states 里面的值 1
、2
、3
对所有线程也是可见的。
但是,初始化安全性的保证,只保证对象在初始化阶段是安全可见的。
如果后面改变的了对象的值,比如在构造函数外执行 state.put("4", "d")
,这个值的可见性就无法保证,必须采用同步。