12.1 Java内存模型
Java内存模型(Java Memory Model)是Java虚拟机规范中定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
12.1.1 主内存与工作内存
- JMM的主要目标是定义程序中各种变量的访问规则
- JMM规定所有的变量都存放在主内存中(虚拟机内存的一部分)
- 每条线程有自己的工作内存,其中保存了被该线程用到的变量的主内存副本拷贝(注意:拷贝的并不是整个对象,而是对象中被用到的字段、该对象的引用等)
- 线程对变量的所有操作都必须在自己的工作内存中,不能直接操作内存中的变量。线程间变量值的传递也必须通过主内存来完成
此处的变量包括实例字段、静态字段和构成数组对象的元素,不包括局部变量和方法参数(形参),因为后者是线程私有的,不会被共享。
主内存和工作内存与java堆、栈等是不同层次的内存划分,所以两者之间没有关系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EwsWdDne-1582538817788)(D:\PersonalNote\JVM\images\工作内存、内存之间的交互关系.png)]
12.1.2 内存间交互操作
关于工作内存与主内存的交互协议,虚拟机定义了8种操作,虚拟机实现时必须保证每一种操作都是原子的、不可再分的:
| 操作 | 作用 |
|---|---|
| lock | 作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线程独占这个变量 |
| unlock | 作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定 |
| read | 作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用 |
| load | 作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的) |
| use | 作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作 |
| assign | 作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作 |
| store | 作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用 |
| write | 作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中 |
把一个变量从主内存传输到工作内存,那就要顺序的执行read和load操作,把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作。虚拟机要求顺序的执行,但并没有要求连续的执行,所以也可能有如下操作:read a; read b; load b; load a
虚拟机还规定了执行上述8中操作是必须满足如下规则:
- 不允许read和load、store和write操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况
- 不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不回写到主内存
- 不允许一个线程无原因地(就是没有发生过任何assign操作)把数据从线程工作内存同步化回主内存
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(也就是没有执行load或者assign操作)的变量。也就是说在执行use、store之前必须对相同的变量执行了load、assign操作
- 一个变量在同一时刻只能被一个线程对其进行lock操作,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同,变量才会被解锁。
- 对变量执行lock操作,将会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值
- 不允许对没有lock的变量执行unlock操作,一个线程也不能对被其他线程lock的变量执行unlock操作
- 对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作
12.1.3 对volatile型变量的特殊规则
对于volatile型变量,在进行read、load、user、assign、store和write操作时需要满足如下规则(T表示一个线程,V和W表示两个volatile型变量):
- 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个操作是use的时候,线程T才能对变量V执行load动作。(这条规则要求在工作内存中,每次使用V之前都必须从主存中刷新最新的值。read->load->use必须连续执行)
- 只有当线程T对变量V执行的前一个动作是assign时,线程T才能对变量V执行store动作;并且,只有当线程T毒变量V执行的后一个动作是store时,线程T才能对变量V执行assign操作。(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主存中。volatile变量后assign->store->write必须连续执行)
- 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。 如果A先于B,那么P先于Q(这条规则保证变量不会被指令重排序优化)
volatile使用场景(不符合场景时使用synchronized或者原子类):
- 运算结果并不依赖变量当前值,或者能够确保还有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
12.1.4 对long和double型变量的特殊规则
JMM允许没有被volatile修饰的64位数据类型(long、double)的读写操作划分为两次32位的操作来进行,即虚拟机实现可以选择不保证64位数据类型的load、store、read、write操作的原子性,称为非原子性协议(目前虚拟机都选择实现为原子性,所以不会发生上述事情)
12.1.5 原子性、可见性和有序性
JMM是围绕着并发过程中如何处理原子性、可见性和有序性来建立的
- 原子性:一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。由JMM直接保证的原子性操作包括read、load、assign、use、store和write(synchronized块之间的操作也具备原子性)
- 可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个值。(volatile、final、synchronized三个关键字可以实现可见性)
- 有序性:即程序执行的顺序按照代码的先后顺序执行。volatile和synchronized关键字可以保证线程之间的有序性
12.1.6 先行发生原则(happens-before)
Java中的有序性除了靠volatile和synchronized完成,还有一个重要的原则–先行发生原则(happens-before)
以下是Java内存模型中天然的先行发生规则,对于不在此列的关系,就没有顺序性保障,虚拟机可以随意的进行重排
- 程序次序原则:在一个线程中,按照代码顺序执行,即书写在前的操作先行执行于后面的操作
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对于一个volatile变量写操作先行发生于后面对这个变量的读操作
- 线程启动规则:Thread对象的start()方法先行发生于线程每一个动作
- 线程终止规则:线程中所有操作先行发生于对线程的终止检查
- 线程中断规则:对线程的interrupt()调用先行发生于被中断线程代码检测到中断事件发生
- 对象终结原则:一个对象的初始化先行于其finalize方法
- 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论
时间的先后顺序与先行发生原则之间没有影响,衡量并发安全问题不受时间顺序影响,一切以先行原则为准
本文详细介绍了Java内存模型(JMM),主要包括主内存与工作内存的概念,内存间交互操作的原子性规则,volatile变量的特殊规则,以及long和double型变量的处理。此外,还阐述了JMM保证的原子性、可见性和有序性,并讨论了先行发生原则(happens-before)在并发中的作用。
519

被折叠的 条评论
为什么被折叠?



