JVM(二) JMM、指令重排等

JSR 规范 Java Memory Model (JSR 133)

JSR133中文版:http://ifeve.com/wp-content/uploads/2014/03/JSR133%E4%B8%AD%E6%96%87%E7%89%881.pdf
JSR133: http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

JMM(Java Memory Model (java内存模型 ≠ java虚拟内存模型))

JMM 是一种符合内存规范的,屏蔽了各种硬件和操作系统访问差异的,保证了Java在各种平台下对内存的访问都能保证效果一致的机制及规范。
提到Java内存模型,一般指的是JDK 5 开始使用的新的内存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。
JMM 规定了所有的变量都储存在主内存中,每条线程都有自己的工作内存,线程的工作内存保证了该线程中用到是变量的内存拷贝。线程对变量的所有操作必须在工作内存中进行,不能直接读写主存。
在这里插入图片描述

不同线程工作内存无法相互访问。线程之间的通信则需要在工作内存和主存中同步进行。
JMM用于工作内存和主存之间数据同步的过程。

CPU和缓存一致性协议

在前文 (https://blog.csdn.net/qq_38895905/article/details/121427573?spm=1001.2014.3001.5502)我们在可见性当中讲了三级缓存,简而言之,计算机当中的每条指令的执行过程都是将数据从主存当中load到cpu本地缓存当中,计算结束后再次写回主存当中。
这个时候,如果是多处理器(MP(Multi_Processors)),就会存在线程之间数据同步的问题。
在这里插入图片描述
这个时候,就引入了一个新的概念,缓存一致性协议。
现代CPU数据一致性的实现实际上就是 缓存锁(缓存一致性协议)+总线锁

内存间交互操作

关于主内存与工作内存之间的具体交互协议,JMM当中规定了以下8种操作来完成,虚拟机实现时必须保证每个操作是原子的、不可再分的(对于double和long类型的变量,load、store、read、write操作在某些平台上可以例外)
lock(锁定): 作用于主内存的变量,把一个变量标识为一条线程独占的状态
unlock(解锁): 作用于主内存的变量,把处于锁定状态的变量释放出来
read(读取): 作用于主内存变量,将变量值从主内存传到工作内存
load(加载): 作用于工作内存的变量,把read操作读来的变量值放入工作内存的变量副本当中
use(使用): 作用于工作内存的变量,把工作内存当中一个变量的值传递给执行引擎。每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
assign(赋值): 作用于工作内存的变量,将从执行引擎接收到的值赋给工作内存的变量副本
store(存储): 作用于工作内存的变量,负责将工作内存的变量副本的值传递到主存当中
write(写入): 作用于主内存的变量,负责将store得到的变量副本值写到主存变量当中

Java规定了使用以上八种基本操作必须满足如下规则:

1.不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现。

2.不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回 主内存。

3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存 中。

4.一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。

5.一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执 行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值。

7.如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个 被其他线程锁定的变量。

8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

指令重排序

从java源码到最终执行指令序列,会经历以下三种重排序:编译器优化重排序,指令集并行重排序,内存系统重排序。
在这里插入图片描述
java允许编译器和未处理器进行优化,这可能会影响到未正确同步的代码。如图:
在这里插入图片描述

如果某次执行表现出了这样的行为,那么我们可能得出这样的结论,指令 4 要在指令 1 之前执行,指令 1 要在指令 2 之前执行,指令 2 要在指令 3 之前执行,指令 3要在指令 4 之前执行。

然而,从单个线程的角度看,只要重排序不会影响到该线程的执行结果,编译器就可以对该线程中的指令进行重排序。如果指令 1 与指令 2 重排序,那就很容易看出为什么会出现 r2 == 2 和 r1 == 1 这样的结果了。 即CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据),去同时执行另一条指令,前提是两条指令没有依赖关系。

内存屏障

可以把内存屏障理解成一堵墙,墙两边的指令无法被Cpu乱序执行。CPU提供了三个汇编指令串行化运行读写保证读写有序性:

SFENCE:在该指令前的写操作必须在该指令后的写操作前完成

LFENCE:在该指令前的读操作必须在该指令后的读操作前完成

MFENCE:在该指令前的读写操作必须在该指令后的读写操作前完成
jvm内存屏障规范

LoadLoad屏障:举例语句是Load1; LoadLoad; Load2(这句里面的LoadLoad里面的第一个Load对应Load1加载代码,然后LoadLoad里面的第二个Load对应Load2加载代码),此时的意思就是,在Load2及后续读取操作从内存读取数据到CPU前,保证Load1从主内存里要读取的数据读取完毕。

StoreStore屏障:举例语句是 Store1; StoreStore; Store2(这句里面的StoreStore里面的第一个Store对应Store1存储代码,然后StoreStore里面的第二个Store对应Store2存储代码)。此时的意思就是在Store2及后续写入操作执行前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见。

LoadStore屏障:举例语句是 Load1; LoadStore; Store2(这句里面的LoadStore里面的Load对应Load1加载代码,然后LoadStore里面的Store对应Store2存储代码),此时的意思就是在Store2及后续代码写入操作执行前,保证Load1从主内存里要读取的数据读取完毕。
***StoreLoad屏障:***举例语句是Store1; StoreLoad; Load2(这句里面的StoreLoad里面的Store对应Store1存储代码,然后StoreLoad里面的Load对应Load2加载代码),在Load2及后续读取操作从内存读取数据到CPU前,保证Store1的写入操作已经把数据写入到主内存里,确认Store1的写入操作对其它处理器可见。

volatile 实现细节

1、字节码
ACC_VOLATILE
2、JVM

volatile写操作 前面加SS屏障 ,后面加SL屏障

	StoreStoreBarrier
	volatile写操作
	StoreLoadBarrier

volatile读操作 前面加LL屏障,后面加LS屏障

LoadLoadBarrier
volatile读操作
LoadStoreBarrier

3.OS
windows 上用的是lock指令实现(没用fence)

Sychronized 实现细节

1.字节码
ACC_SYCHRONIZED
monitorenter monitorexit
2.JVM
通过一些c和c++指令实现
3.OS
x86: lock cmpxchg

happens-before

两个动作(action)可以被 happens-before 关系排序。如果一个动作 happens-before 另一个动作,则第一个对第二个可见,且第一个排在第二个之前。必须强调的是,两个动作之间存在 happens-before 关系并不意味着这些动作在 Java 中必须以这种顺序发生。happens-before 关系主要用于强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。可以通过多种方式包含一个happens-before 顺序,包括:

1.某个线程中的每个动作都 happens-before 该线程中该动作后面的动作。
2.某个管程上的 unlock 动作 happens-before 同一个管程上后续的 lock 动作。
3.对某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作。
4.在某个线程对象上调用 start()方法 happens-before 该启动了的线程中的任意动作。
5.某个线程中的所有动作 happens-before 任意其它线程成功从该线程对象上的join()中返回。
6.如果某个动作 a happens-before 动作 b,且 b happens-before 动作 c,则有 a happens-before c

在这里插入图片描述
如图,对同一个程序的不同执行路径中,b的执行对x的冲突访问没用happens-before顺序,因此这个程序是未同步的。

as-if-serial

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。

double pi  = 3.14;    //A  
double r   = 1.0;     //B  
double area = pi * r * r; //C 

此处A与C有依赖关系,B与C也有依赖关系,但是A与B并没有依赖关系。此处就是需要无论先执行A还是先执行B,最终的结果都一样。
在这里插入图片描述
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

final字段

声明为 final 的字段初始化一次后,在正常情况下它的值不会再改变。
final 字段也允许编程人员在不需要同步的情况下实现线程安全的不可变对象。一个线程安全的不可变对象被所有线程都视为不可变的,即使不可变对象的引用在线程间传递时存在数据争用。这提供了安全保证,可以防止不正确或恶意代码误用了不可变类。final字段也保证了可见性。

final 字段必须正确使用才能保证不可变。当对象的构造器执行结束,就认为该对象是完全初始化了的。一个线程只有在看到某个对象引用之前,该对象就已经完全初始化了,才能保证这个线程能看到该对象正确初始化的 final 字段值。(构造器不会把初始化一半的对象传递出去,不会发生“this溢出”)
在这里插入图片描述

顺序一致性

顺序一致性是程序执行过程中可见性和顺序的强有力保证。在顺序一致性的执行过程中,所有动作(如读和写)间存在一个全序关系,与程序顺序一致。
每个动作都是原子的且立即对所有线程可见

顺序一致的内存模型

正式地,在顺序一致性里,所有动作以全序(执行顺序)的顺序发生,与程序顺序一致;而且,每个对变量 v 的读操作 r 都将看到写操作 w 写入 v 的值,只要:
 执行顺序上 w 在 r 之前,且
 执行顺序上不存在这样一个 w’,w 在 w’之前且 w’在 r 之前。
顺序一致性太严格了,不适合做 Java 内存模型,因为它禁止了标准的编译器和处理器优化。

参考资料

JSR规范整理:https://www.iteye.com/blog/manysysy-1046963
JSR133中文版:http://ifeve.com/wp-content/uploads/2014/03/JSR133%E4%B8%AD%E6%96%87%E7%89%881.pdf
JSR133: http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf
《深入理解Java虚拟机》第12章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值