我们知道,重排序的目的是在不改变程序执行结果的前提下,提高编译器和处理器对程序的执行性能。但是,重排序不是任意的,所谓无规矩不成方圆。理解重排序就需要知道重排序必须遵守的规则,总结起来就是我们今天要说的Happens-Before规则。在JSR-133: JavaTM Memory Model and Thread Specification中有相关描述,原版英文请见pdf文件,下载了一份供大家学习。
一. Happens-Before规则
Happens-Before规则规定了哪些情况下指令不能进行重排序。
- 程序顺序原则:一个线程内,代码执行的过程必须保证语义的串行性( as-if-serial,看起来是串行的;另外如果程序内数据存在依赖,也不允许进行重排序 )。
- 监视器锁规则:解锁unlock必然发生在加锁lock前。
- 传递性规则:如果操作A先于操作B,操作B先于操作C,那么操作A必先于操作C。
- volatile规则:一个共享变量的写操作,必须先于读操作,这是volatile可见性语义的要求。
- 线程的start规则:线程的start操作先于线程内其他任何操作。
- 线程的join规则:如果线程ThreadA中执行了ThreadB.join()方法,那么ThreadB的所有操作先于ThreadA中ThreadB.join()返回后的操作。
二. 对于Happens-Before规则解释
1. as-if-serial语义
看起来像串行的--编译器和处理器对重排序的机制对程序员是透明的,但是我们观察到的结果跟按照编写程序的顺序是一致的,这就是看起来像的含义。
举一例说明:
int a = 2; // A int b = 3; // B int c = a*b; // C
在上述程序中,步骤C依赖于步骤A和B,但是步骤A和B之间没有依赖关系,依赖关系图长这样:
根据程序的顺序执行规则,由于C依赖于A和B,那么C的执行顺序不能排在A和B之前,但是A和B的顺序是可以互换的,也就是说,我们按照程序顺序执行的语义,看到的执行顺序是这样:A-->B-->C,但是编译器和处理器可能进行重排序成这样子:B-->A-->C,但是这个过程对我们来说是透明的,但是最终结果跟我们想要的是一样的。这就是看起来像 as-if-serial的语义。
2.锁规则
在并发编程中,锁保证了临界区的互斥访问,同时还可以让释放锁的线程向另一个线程发送消息。
我们举一例,先来段代码。
public class MonitorDemo { int a = 0; public synchronized void writer() { // 1 a++; // 2 } // 3 public synchronized void reader() { // 4 int i = a; // 5 …… } // 6 }
比如现在有两个线程A和B,线程A执行writer方法,线程B随后执行reader方法,根据happens-before原则,我们来梳理下这个过程包含的happens-before关系。
①依据程序顺序执行顺序原则,1-->2-->3;4-->5-->6
②根据监视器锁规则,锁的获取先于锁的释放,那么在A线程未执行完writer时,线程B是无法得到锁的。因此3-->4.
③根据传递性规则,那我们可以得到2-->5.
最后我们得到的happens-before关系图是这样子的:
这也就是说,当线程B获取到线程A释放的锁后,线程A操作过的共享变量的内容对B是可见的(线程A的步骤2改变了a的值,线程B的步骤5获得了同一把锁后立刻可以得到a的最新值)。
这里我们也对在并发编程中锁的语义进行总结:
-
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
-
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前共享变量所做修改的)消息。
-
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
3.volatile规则
3.1 volatile语义
在并发编程中,单个volatile变量的读、写可以看成是使用同一把锁对单个变量读写操作进行了同步锁操作。
例如,线程A和线程B执行下列代码,线程A执行set()方法,线程B随后执行get()方法。使用volatile
变量和对普通变量进行操作加锁的执行效果是一致的。
下面两个代码执行效果是等价的:
public class VolatileDemo { volatile long vl = 0L; // 使用volatile声明64位的long型变量 public void set(long l) { vl = l; //1. 单个volatile变量的写 } public void getAndIncrement () { vl++; // 复合(多个)volatile变量的读/写 } public long get() { return vl; //2. 单个volatile变量的读 } } public class VolatileDemo2 { long vl = 0L; // 64位的long型普通变量 public synchronized void set(long l) { // 对单个的普通变量的写加同步锁 vl = l; } public void getAndIncrement () { // 普通方法调用 long temp = get(); // 调用已同步的读方法 temp += 1L; // 普通写操作 set(temp); // 调用已同步的写方法 } public synchronized long get() { // 对单个的普通变量的读加同步锁 return vl; } }换句话说,volatile变量的写与锁的获取有相同的内存语义,volatile变量的读与锁的释放有相同的内存语义,这也就证明了对单个volatile变量的读写操作是原子性的,但是对volatile变量进行复合操作不具有原子性的,这个一定要注意。
我们来梳理下使用volatile变量的happens-before关系图,可能对理解更有帮助。
来个例子,线程A和线程B执行下列代码,线程A执行set()方法,线程B随后执行get()方法:
public class VolatileDemo { volatile long vl = 0L; // 使用volatile声明64位的long型变量 public void set(long l) {//1 vl = l; // 2 } public long get() {//3 return vl; // 4 } }
①根据程序顺序执行原则,1-->2,;3-->4
②根据volatile规则,volatile变量的写先于读,所以2-->3
③根据传递性规则,1-->4
所以,我们最后得到的happens-before关系图是这样的:
总结一下,volatile的内存语义:
- 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
- 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
- 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
3.2 volatile语义的实现
我们先来看看编译器制定的volatile重排序规则表。
在规则表中,我们可以明确看到,
当第一个操作是volatile读时,不管第二个操作是什么都不能重排序。
当第一个操作是volatile写时,第二个操作为volatile读、写时不能重排序。
为了实现volatile的语义,编译器在编译代码时候,会生成对应的内存屏障指令,来禁止特定类型操作的处理器重排序。JMM采用保守(认为每个都必须这么做)的内存屏障插入策略来实现volatile语义:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
举一例子体会下:
public class VolatileBarrierDemo { int c; volatile int a = 1; volatile int b = 2; void readAndWrite() { int i = a; // 第一个volatile读 int j = b; // 第二个volatile读 c = i + j; // 普通写 a = i + 1; // 第一个volatile写 b = j * 2; // 第二个 volatile写 } }
最后生成的指令执行示意图如下(红色部分的屏障可以省略掉,因为紧跟着的操作跨越不了已有的屏障):
相关推荐
轴类零件加工工艺设计.zip
资源内项目源码是来自个人的毕业设计,代码都测试ok,包含源码、数据集、可视化页面和部署说明,可产生核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果、标签分布图。都是运行成功后才上传资源,毕设答辩评审绝对信服的保底85分以上,放心下载使用,拿来就能用。包含源码、数据集、可视化页面和部署说明一站式服务,拿来就能用的绝对好资源!!! 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、大作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.txt文件,仅供学习参考, 切勿用于商业用途。
seaborn基本绘图人力资源数据集
移动机器人(sw三维)
自制html网页源代码查看器
3吨叉车的液压系统设计().zip
1_实验三 扰码、卷积编码及交织.ppt
北京交通大学软件学院自命题科目考试大纲.pdf
雅鲁藏布江流域 shp矢量数据 (范围+DEM).zip
基于RUST的数据结构代码示例,栈、队列、图等
NIFD:2024Q1房地产金融报告
详细介绍及样例数据:https://blog.csdn.net/li514006030/article/details/146916652
【工业机器视觉定位软件Vision-Detect】基于C#的WPF与Halcon开发的工业机器视觉定位软件(整套源码),开箱即用 有用户登录,图片加载,模板创建,通讯工具,抓边抓圆,良率统计,LOG日志,异常管理,九点标定和流程加载保存等模块,功能不是很完善,适合初学者参考学习。 资源介绍请查阅:https://blog.csdn.net/m0_37302966/article/details/146912206 更多视觉框架资源:https://blog.csdn.net/m0_37302966/article/details/146583453
内容概要:本文档详细介绍了Java虚拟机(JVM)的相关知识点,涵盖Java内存模型、垃圾回收机制及算法、垃圾收集器、内存分配策略、虚拟机类加载机制和JVM调优等内容。首先阐述了Java代码的编译和运行过程,以及JVM的基本组成部分及其运行流程。接着深入探讨了JVM的各个运行时数据区,如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区等的作用和特点。随后,文档详细解析了垃圾回收机制,包括GC的概念、工作原理、优点和缺点,并介绍了几种常见的垃圾回收算法。此外,文档还讲解了JVM的分代收集策略,新生代和老年代的区别,以及不同垃圾收集器的工作方式。最后,文档介绍了类加载机制、JVM调优的方法和工具,以及常用的JVM调优参数。 适合人群:具备一定Java编程基础的研发人员,尤其是希望深入了解JVM内部机制、优化程序性能的技术人员。 使用场景及目标:①帮助开发人员理解Java代码的编译和执行过程;②掌握JVM内存管理机制,包括内存分配、垃圾回收等;③熟悉类加载机制,了解类加载器的工作原理;④学会使用JVM调优工具,掌握常用调优参数,提升应用程序性能。 其他说明:本文档内容详尽,适合用作面试准备材料和技术学习资料,有助于提高开发人员对JVM的理解和应用能力。
Android项目原生java语言课程设计,包含LW+ppt
戴德梁行&中国房地产协会:2021亚洲房地产投资信托基金研究报告
Android项目原生java语言课程设计,包含LW+ppt
Thinkphp6.0+vue个人虚拟物品发卡网站源码 支持码支付对接 扫码自动发货 源码一共包含两个部分thinkphp6.0后端文件,以及vue前端文件.zip
《基于YOLOv8的食品冷链运输车厢门未锁闭预警系统》(包含源码、可视化界面、完整数据集、部署教程)简单部署即可运行。功能完善、操作简单,适合毕设或课程设计
资源内项目源码是来自个人的毕业设计,代码都测试ok,包含源码、数据集、可视化页面和部署说明,可产生核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果、标签分布图。都是运行成功后才上传资源,毕设答辩评审绝对信服的保底85分以上,放心下载使用,拿来就能用。包含源码、数据集、可视化页面和部署说明一站式服务,拿来就能用的绝对好资源!!! 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、大作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.txt文件,仅供学习参考, 切勿用于商业用途。