Java内存模型
2023-07-19 本文已影响0人
追风还是少年
Java内存模型(java memory model,JMM),目的是为了屏蔽各种硬件及操作系统的内存访问差异,实现java程序在各个平台下都能达到一致的内存访问效果
物理内存模型 JMMJava内存模型可以与物理内存模型做类比,CPU-线程、高速缓存-工作内存
- 主内存:所有变量都存储在主内存
- 工作内存:每个线程有自己的工作内存,工作内存保存了线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主内存中的数据
原子性
原子即不能被进一步分割的最小粒子,原子操作即不可中断的一个或一序列操作
JMM定义了8种操作规范来完成一个变量从主内存拷贝到工作内存、从工作内存同步回工作内存的实现细节
JMM定义的8种原子操作:
- lock-锁定:作用于主内存,把一个变量标识为一条线程独占的状态。
- unlock-解锁:作用于主内存,把一个变量从锁定状态释放出来,释放后的变量才可以被其它线程锁定。
- read-读取:作用于主内存,把一个变量的值从主内存传输到工作内存,以便随后的load操作使用
- load-载入:作用于工作内存,把read操作得到的值放入工作内存的变量副本种
- use-使用:作用于工作内存,把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign-赋值:作用于工作内存,把从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store-存储:作用于工作内存,把工作内存中的一个变量的值传送到主内存,以便随后的write操作使用
- write-写入:作用于主内存,把store操作从工作内存中得到的变量的值放入主内存的变量
非原子操作即线程不安全的,如:
- i++,i--是非原子的,可以拆分为读取i的值、i的值加(减)1、写回新值
- i=j,是非原子的,可以拆分为读取j的值、把j的值赋值给i
如何保证原子性?
- 在处理器层面,通过提供总线锁、缓存锁来保证
- 在java编程语言层面,提供了锁和CAS来保证
java内存模型来直接保证6个变量操作的原子性,包括read、load、use、assign、store、write,基本数据类型的访问、读写都具备原子性(例外是long、double非原子性协定)。
更大访问的原子性保证,需要使用syncronized关键字(java语言层面的锁,对应字节指令monitorenter、monitorexit)、juc的Lock接口(类库层面的锁)来实现
可见性
什么是可见性?即指当一个线程修改共享变量的值,其它线程能够立即得知这个修改。
JMM从上图来看,线程A和线程B之间要通信的话,需要经过下面两步:
(1)线程A把工作内存A中更新过的共享变量刷新到主内存
(2)线程B到主内存去读取线程A之前已更新过的共享变量
如何保证可见性?
使用volatile、syncronized、final三个java关键字能保证可见性
- 规则:对一个变量进行unlock操作前,必须把变量的值同步回主内存(执行store、write操作),该规则正是syncronized保证原子性、可见性的理论支撑
- final:final修饰的字段在构造器中一旦初始化完成,就不能修改了,那么在其它线程中就能看见final字段的值
有序性
什么是有序性?
- CPU可能会对输入代码做乱序执行优化,即不能保证各个语句计算的先后顺序与输入代码中的顺序一致。
- 在重排序时,CPU和编译器都需要遵守一个规矩,这个规矩就是as if serial语义,即不管怎么重排序,在单线程环境下的执行结果不能被改变。‘
CPU和编译器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
不同CPU之间和不同线程之间的数据依赖性是不被CPU和编译器考虑的。- java的编译器有这样一种优化手段:指令重排序。
如何保证有序性
- volatile:除了保证可见性外,还包含禁止指令重排序的语义
- syncronized:被syncronized修饰的代码是单线程执行的,所有这就满足了as if serial 语义的单线程的前提,有了as if serial 语义的保证,单线程有序性也就得到保证
- happens-before(先行发生)原则:是JMM的灵魂,它是判断数据是否存在竞争、线程是否安全的非常有用的手段,如果java内存模型中的所有有序性都仅靠volatile和syncronized来完成,那么很多操作都将会变的非常啰嗦,我们在编写java并发代码时并没用察觉到这一点,这就归功于“happens-before”
happens-before
happens-before定义:
- 如果一个操作happens-before另一个操作,那么第一个操作的结果对第二操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须按照happens-before关系指定的顺序来执行,如果重排序之后的执行结果,与按照happens-before关系来执行的结果一样,那么这种重排序并不非法的(也就是说,jvm允许这种重排序)
注意:不同于as-if-serial语义只能作用在单线程,happens-before提供跨线程的内存可见性保证。
八条happens-before规则:
- 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作,这个规则是针对syncronized的
- volatile变量规则:一个volatile变量的写操作先行发生于后面对同一个volatile变量的读操作
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作先行发生于对此线程的终止检测操作,我们可以通过 Thread 对象的 join() 方法检测是否结束、Thread 对象的 isAlive() 的返回值等手段检测线程是否已经终止执行。
- 线程中断规则:对线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread对象的interrupted()方法检测是否有中断发生
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
- 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C的结论
- 一个操作“时间上的先发生”,不代表这个操作会是“先行发生”
- 一个操作“先行发生”,不代表这个操作一定是“时间上的先发生”
如:
int i=1;
int i=2;
根据程序次序规则,“int i = 1” 的操作先行发生(Happens-before)于 “int j = 2”,但是只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。
所以,“int j=2” 这句代码完全可能优先被处理器执行,因为这并不影响程序的最终运行结果。- 结论:Happens-before 原则与时间先后顺序之间基本没有因果关系,所以我们在衡量并发安全问题的时候,尽量不要受时间顺序的干扰,一切必须以 Happens-before 原则为准。
Happens-before 与 as-if-serial
- Happens-before: 即只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。
- as-if-serial 语义:不管怎么重排序,单线程环境下程序的执行结果不能被改变
- 对比:本质上来说 Happens-before 关系和 as-if-serial 语义是一回事,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。只不过后者只能作用在单线程,而前者可以作用在正确同步的多线程环境下:
as-if-serial 语义保证单线程内程序的执行结果不被改变,Happens-before 关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial 语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。
Happens-before 关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按 Happens-before 指定的顺序来执行的。