写点什么

深入理解 Java 内存模型,小白也能看得懂!,限时发布

用户头像
极客good
关注
发布于: 刚刚

主内存和工作内存数据交互(原子性:八种原子性操作和八条原则)


==============================


八种原子性操作


对于物理机来说,高速缓存和主内存之间的交互有协议,同样的,Java 内存中每个线程的工作内存和 JVM 占用的主内存的交互是由 JVM 定义了如下的 8 种操作来完成的,每种操作必须是原子性的。JVM 中主内存和工作内存交互,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存。


1)lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线程独占这个变量


2)unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定


3)read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的 load 操作使用(解释:主内存–>工作内存,读取主内存)


4)load(载入):作用于线程的工作内存的变量,表示把 read 操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)(解释:主内存–>工作内存,写入工作内存)


5)use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作(解释:工作内存–>执行引擎,变量操作)


6)assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作(解释:执行引擎–>工作内存,变量赋值)


7)store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的 write 操作使用(解释:工作内存–>主内存,读取工作内存)


8)write(写入):作用于主内存的变量,把 store 操作从工作内存中得到的变量的值放入主内存的变量中(解释:工作内存–>主内存,写入主内存)


对于 Java 程序中的变量读取语句,要把一个变量从主内存传输到工作内存,就要顺序的执行 read 和 load 操作;


对于 Java 程序中的变量写入语句,要把一个变量从工作内存回写到主内存,就要顺序的执行 store 和 write 操作。


八条规则


对于普通变量,虚拟机只是要求顺序的执行,并没有要求连续的执行,所以如下也是正确的。对于两个线程,分别从主内存中读取变量 a 和 b 的值,并不一样要 read a; load a; read b; load b;


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


也会出现如下执行顺序:read a; read b; load b; load a; 对于这 8 种操作,虚拟机也规定了一系列规则,在执行这 8 种操作的时候必须遵循如下的规则:


1)read 和 load、store 和 write:对于 Java 程序中的读取和写入,不允许 read 和 load、store 和 write 操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况


2)assign:对于 Java 程序中的执行引擎运算返回结果,不允许一个线程丢弃最近的 assign 操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存


3)assign:不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何 assign 操作,是不允许将该变量的值回写到主内存


4)use-load,store-assign:先后原则,变量只能在主内存中产生,源头必须是主内存,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行 load 或者 assign 操作,也就是说在执行 use、store 之前必须对相同的变量执行了 load、assign 操作


5)lock:一个变量在同一时刻只能被一个线程对其进行 lock 操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。


6)lock:对变量执行 lock 操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新 load 或者 assign 操作初始化变量的值


7)unlock:不允许对没有 lock 的变量执行 unlock 操作,如果一个变量没有被 lock 操作,那也不能对其执行 unlock 操作,当然一个线程也不能对被其他线程 lock 的变量执行 unlock 操作


8)unlock:对一个变量执行 unlock 之前,必须先把变量同步回主内存中,也就是执行 store 和 write 操作


当然,最重要的还是如开始所说,这 8 个动作必须是原子的,不可分割的。


分解 Java 程序练习


常量读取零步操作,变量读取一步操作;


常量赋值一步操作,变量赋值两步操作;


常量计算并写入两步操作,变量计算并写入三步操作。


解释(八个原子性操作):


常量读取零步操作,啥都不干


变量读取一步操作,读取变量 a,主内存–>工作内存,先读取主内存 read,再写入工作内存 load,根据下面规则 1,两个不能拆开,所以变量读取是原子操作。


常量赋值一步操作,int a=1,工作内存–>主内存,先读取工作内存 store,再写入主内存 write,根据下面规则 1,两个不能拆开,所以常量赋值给变量是原子操作。


变量赋值两步操作,int b=a,先变量 a 主内存–>工作内存,然后变量 b 工作内存–>主内存,两步操作。


常量计算并写入两步操作,int a=1+1,先 1+1=2 返回结果,执行引擎–>工作内存,使用 assign 命令,然后变量 a 工作内存–>主内存,总共两步操作。


变量计算并写入三步操作,int b=a+1,先变量 a 主内存–>工作内存,然后 a+1 返回结果,执行引擎–>工作内存,最后变量 b 工作内存–>主内存,三步操作。


注意,先用 int,先不考虑 long 和 double,这两个 64 位的。


long double 型变量的特殊规则


Java 内存模型要求对主内存和工作内存交换的八个动作是原子的,正如上面所讲,但是对 long 和 double 有一些特殊规则。原因是什么呢?


其实,问题倒不是出现在 8 个动作上,这个 8 个动作是确实是原子性操作,这一点是毋庸置疑的,问题出在 long 和 double 这两种基本数据类型上。


八个动作中 lock、unlock、read、load、use、assign、store、write 对待 32 位的基本数据类型都是原子操作,对待 long 和 double 这两个 64 位的数据,java 虚拟机规范对 java 内存模型的规定中特别定义了一条相对宽松的规则:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,也就是允许虚拟机不保证对 64 位数据的 read、load、store 和 write 这 4 个动作的操作是原子的。


这也就是我们常说的 long 和 double 的非原子性协定(Nonautomic Treatment of double and long Variables)。


原子性、可见性与有序性


===========


Java 内存模型是围绕着并发过程中如何处理原子性、可见性和有序性 3 个特征建立的。


1)原子性:


由 Java 内存模型来直接保证原子性的变量操作包括 read、load、use、assign、store、write 这 6 个动作,虽然存在 long 和 double 的特例,但基本可以忽略不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock 和 unlock 也可以满足需求。


lock 和 unlock 虽然没有被虚拟机直接开放给用户使用,但是提供了字节码层次的指令 monitorenter 和 monitorexit 对应这两个操作,对应到 java 代码就是 synchronized 关键字,因此在 synchronized 块之间的代码都具有原子性(这是程序员所熟知的)。


2)可见性:


可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile 类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。


除了 volatile,synchronized 和 final 也可以实现可见性。synchronized 关键字是通过 unlock 之前必须把变量同步回主内存来实现的,final 则是在初始化后就不会更改,所以只要在初始化过程中没有把 this 指针传递出去也能保证对其他线程的可见性。


3)有序性:


有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句指的是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。


保证有序性的关键字有 volatile 和 synchronized,volatile 禁止了指令重排序,而 synchronized 则由“一个变量在同一时刻只能被一个线程对其进行 lock 操作”来保证。


总体来看,synchronized 对三种特性(原子性、可见性、有序性)都有支持,虽然简单,但是如果无控制的滥用对性能就会产生较大影响。


synchronized 关键字是绝对安全的,因为它可以同时保证原子性、可见性、有序性,但是这并不意味着 synchronized 关键字可以随意使用,事实上,synchronized 是一种重量级锁,对性能的影响还是比较大的,本文第五部分介绍锁优化就是为了解决 synchronized 重量级锁的性能损耗问题。


有序性:先行发生原则


==========


有序性:八条先行发生原则


Java 内存模型具备一些 先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性 ,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从 happens-before 原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。


下面就来具体介绍下 happens-before 原则(先行发生原则):


(1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;


(2)锁定规则:一个 unLock 操作先行发生于后面对同一个锁额 lock 操作;


(3)volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;


(4)传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C;


(5)线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作;

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
深入理解Java内存模型,小白也能看得懂!,限时发布